local mymodule = {} -- Load libraries modelfunctions = require("modelfunctions") posix = require("posix") fs = require("acf.fs") format = require("acf.format") validator = require("acf.validator") db = require("acf.db") session = require("session") -- Set variables local DatabaseName = "provisioning" local DatabaseUser = "postgres" local DatabasePassword local baseurl = "/etc/provisioning/templates/" local updatedevicescriptfile = "/etc/provisioning/update_device.lua" local updatedeviceparamsscriptfile = "/etc/provisioning/update_device_params.lua" local deletedevicescriptfile = "/etc/provisioning/delete_device.lua" local determineclassscriptfile = "/etc/provisioning/determine_class.lua" local processputscriptfile = "/etc/provisioning/process_put.lua" local scriptfiles = {updatedevicescriptfile, updatedeviceparamsscriptfile, deletedevicescriptfile, determineclassscriptfile, processputscriptfile} local provdb = db.create(db.engine.postgresql, DatabaseName, DatabaseUser, DatabasePassword) provdb.table_creation_scripts = require("provisioning/provisioning-scripts") local saved_devices = {} local saved_device_params = {} -- These are the functions that may be called from within loaded Lua code -- The functions must be added after they're declared local functions -- ################################################################################ -- LOCAL FUNCTIONS -- This function is used by scripts, do not change prototype local runsqlcommand = function(sql, transaction) mymodule.logevent(sql) return provdb.runsqlcommand(sql, transaction) end -- This function is used by scripts, do not change prototype local getselectresponse = function(sql, transaction) mymodule.logevent(sql) return provdb.getselectresponse(sql, transaction) end local function assert (v, m) if not v then m = m or "Assertion failed!" error(m, 0) end return v, m end local createdatabase = function() local result = {} local cmdresult, errtxt -- First, create the user if DatabaseUser ~= "postgres" then local cmd = "CREATE USER "..DatabaseUser if DatabasePassword then cmd = cmd .. " WITH PASSWORD '"..DatabasePassword.."'" end cmdresult, errtxt = modelfunctions.run_executable({"psql", "-U", "postgres", "-c", cmd}, true) table.insert(result, errtxt) table.insert(result, cmdresult) end -- Create the database local cmd = "CREATE DATABASE "..DatabaseName.." WITH OWNER "..DatabaseUser cmdresult, errtxt = modelfunctions.run_executable({"psql", "-U", "postgres", "-c", cmd}, true) table.insert(result, errtxt) table.insert(result, cmdresult) cmdresult, errtxt = modelfunctions.run_executable({"createlang", "-U", "postgres", "plpgsql", DatabaseName}, true) table.insert(result, errtxt) table.insert(result, cmdresult) mymodule.logevent(table.concat(result, "\n")) return table.concat(result, "\n") end -- Delete the database and roles local deletedatabase = function() local result = {} local cmd = "DROP DATABASE "..DatabaseName local cmdresult, errtxt = modelfunctions.run_executable({"psql", "-U", "postgres", "-c", cmd}, true) table.insert(result, errtxt) table.insert(result, cmdresult) if DatabaseUser ~= "postgres" then cmd = "DROP ROLE "..DatabaseUser local cmdresult, errtxt = modelfunctions.run_executable({"psql", "-U", "postgres", "-c", cmd}, true) table.insert(result, errtxt) table.insert(result, cmdresult) end mymodule.logevent(table.concat(result, "\n")) return table.concat(result, "\n") end local databaseconnect = function() local result = false local res, err = pcall(function() result = provdb.databaseconnect() end) if not res and err then if string.match(err, 'Error connecting to database.') or string.match(string.lower(err), 'database "provisioning" does not exist') then createdatabase() result = provdb.databaseconnect() -- Let's create all the tables from the start for n,v in pairs(provdb.table_creation_scripts) do if not string.match(n, "^_") then runsqlcommand("SELECT * FROM "..provdb.escape(n).." LIMIT 1") end end else assert(res, err) end end return result end local validateparam validateparam = function(p, allowdefault) if p.type == "group" then local success = true for n,p2 in pairs(p.value) do success = validateparam(p2, allowdefault) and success end return success elseif allowdefault and p.value == p.default then return true elseif p.type == "select" then return modelfunctions.validateselect(p) elseif p.type == "text" and p.regexp and p.regexp ~= "" then if not string.match(p.value, p.regexp) then p.errtxt = "Invalid value" return false end end return true end local validateparamcoded validateparamcoded = function(p, top) top = top or p local success = true if p.type == "group" then for n,p2 in pairs(p.value) do success = validateparamcoded(p2, top) and success end return success end if p.validate and p.validate ~= "" then -- We have Lua validation code local f local env = {} setmetatable (env, {__index = _G}) local IS_52_LOAD = pcall(load, '') if IS_52_LOAD then f = load(p.validate, nil, "bt", env) else -- loadfile loads into the global environment -- so we set env 0, not env 1 setfenv (0, env) f = loadstring(p.validate) setfenv (0, _G) end if (f) then functions.logevent = mymodule.logevent local res, err = pcall(function() p.value, p.errtxt = f(p.value, functions, top) if p.errtxt then success = false end end) if not res and err then success = false p.errtxt = "Exception in validate code\n"..err end end end return success end local function callscript(script, ...) local result={} local f local env = {} setmetatable (env, {__index = _G}) local IS_52_LOAD = pcall(load, '') if IS_52_LOAD then f = loadfile(script, "bt", env) else -- loadfile loads into the global environment -- so we set env 0, not env 1 setfenv (0, env) f = loadfile(script) setfenv (0, _G) end if f then functions.logevent = mymodule.logevent local res, err = pcall(function(...) result = { f(functions, ...) } end, ...) if not res and err then assert(res, "Exception in "..script.." script\n"..err) end elseif posix.stat(script) then -- file exists, but wouldn't load error("Failed to load "..script, 0) end return unpack(result) end local function validateluacode(code) local success = true -- Validate that contents are valid lua code local f,errtxt local env = {} setmetatable (env, {__index = _G}) local IS_52_LOAD = pcall(load, '') if IS_52_LOAD then f = load(code, nil, "bt", env) else -- loadfile loads into the global environment -- so we set env 0, not env 1 setfenv (0, env) f,errtxt = loadstring(code) setfenv (0, _G) end if not f then success = false end return success, errtxt end local function validatefiledetails(filedetails) local success, errtxt = validateluacode(filedetails.value.filecontent.value) if not success then filedetails.value.filecontent.errtxt = "Invalid Lua code\n"..(errtxt or "") end return success, filedetails end local duplicatestructure duplicatestructure = function(value, saved) saved = saved or {} if type(value) == "table" then if saved[value] then return saved[value] else local output = {} saved[value] = output for k,v in pairs(value) do output[k] = duplicatestructure(v, saved) end return output end else return value end end local checkgroupdefaultoverride = function(device_id) sql = "SELECT p.param_id ".. "FROM (devices_to_classes d2t JOIN provisioning_classes t USING(class_id) JOIN classes_to_param_groups t2g USING (class_id) JOIN provisioning_groups g USING(group_id) ".. "JOIN param_groups_to_params g2p USING(group_id) JOIN provisioning_params p USING(param_id) JOIN provisioning_values v USING(param_id))".. "WHERE d2t.device_id='"..provdb.escape(device_id).."' AND v.value IS NOT NULL AND g2p.value IS NOT NULL AND v.value!=g2p.value" local tmp = getselectresponse(sql) return (#tmp ~= 0) end -- This function is used by scripts, do not change prototype local get_device = function(device_id, create) local retval = {} retval.device_id = cfe({value=device_id or "", label="Device ID", seq=1}) retval.classes = cfe({type="group", value={}, label="Classes", seq=2}) local errtxt local res, err = pcall(function() local classes={} local connected = databaseconnect() if not create and device_id and device_id ~= "" then -- Get the device-to-class mappings local sql = "SELECT class_id FROM devices_to_classes WHERE device_id='"..provdb.escape(device_id).."'" local tmp = getselectresponse(sql) for i,g in ipairs(tmp) do classes[g.class_id] = true end end -- Finally, get the class options sql = "SELECT class_id, g.name, g.label AS group, c.label, c.seq FROM provisioning_classes c JOIN provisioning_class_groups g USING(class_group_id) ORDER BY g.seq ASC, g.label ASC, c.seq ASC, c.label ASC" tmp = getselectresponse(sql) for i,c in ipairs(tmp) do if not retval.classes.value[c.name] then retval.classes.value[c.name] = cfe({type="select", label=c.group, option={{value="", label=""}}, seq=i}) end local class = retval.classes.value[c.name] class.option[#class.option + 1] = {value=c.class_id, label=c.label} if classes[c.class_id] then class.value = c.class_id end end if connected then provdb.databasedisconnect() end end) if not res and err then errtxt = err end -- Save the device for later use local output = cfe({ type="group", value=retval, label="Provisioning Device", errtxt=errtxt }) if not create and device_id and device_id ~= "" then saved_devices[device_id] = duplicatestructure(output) end return output end -- This function is used by scripts, do not change prototype local get_device_params = function(device_id, editable) local retval = {} retval.device_id = cfe({value=device_id or "", label="Device ID", seq=0}) local errtxt = "Cannot find device" if device_id and device_id ~= "" then local res, err = pcall(function() local connected = databaseconnect() -- First, just check to see if device_id exists local sql = "SELECT * FROM devices_to_classes WHERE device_id='"..provdb.escape(device_id).."' LIMIT 1" local tmp = getselectresponse(sql) if tmp and #tmp > 0 then errtxt = nil -- Next, get all of the param groups sql = "SELECT * FROM provisioning_groups" local tmp = getselectresponse(sql) -- Loop through the groups and put them into the result for i,g in ipairs(tmp) do retval[g.name] = g retval[g.name].label = g.name retval[g.name].type="group" end -- Then, get all of the parameters for this device sql = "SELECT g.name AS group, g.label AS grouplabel, p.param_id, p.name, p.type, p.label, p.descr, p.seq, p.regexp, p.validate, CASE WHEN v.value IS NOT NULL THEN v.value WHEN g2p.value IS NOT NULL THEN g2p.value ELSE p.value END AS value, CASE WHEN g2p.value IS NOT NULL THEN g2p.value ELSE p.value END AS default, g2p.value AS groupdefault ".. "FROM (devices_to_classes d2t JOIN provisioning_classes t USING(class_id) JOIN classes_to_param_groups t2g USING (class_id) JOIN provisioning_groups g USING(group_id) ".. "JOIN param_groups_to_params g2p USING(group_id) JOIN provisioning_params p USING(param_id)) LEFT JOIN provisioning_values v ON(d2t.device_id=v.device_id AND p.param_id=v.param_id AND g.name=v.group_name ) ".. "WHERE d2t.device_id='"..provdb.escape(device_id).."'" if editable then sql = sql.." AND g2p.editable='t'" end local tmp = getselectresponse(sql) -- Loop through the params to figure out options and put them into the groups for i,p in ipairs(tmp) do -- Options if (p.type == "select") then sql = "SELECT * FROM provisioning_options WHERE param_id='"..provdb.escape(p.param_id).."' ORDER BY seq ASC" p.option = getselectresponse(sql) or {} end -- Groups if not retval[p.group].value then retval[p.group].value = {} end retval[p.group].label = p.grouplabel local value = retval[p.group].value if p.type == "boolean" then p.value = (p.value == "true") p.default = (p.default == "true") if p.groupdefault then p.groupdefault = (p.groupdefault == "true") end end value[p.name] = p end -- Finally, loop through the result and remove empty groups for name,val in pairs(retval) do if not val.value then retval[name] = nil end end -- Even more finally, add in a flag to show if group defaults have been overridden retval.groupdefaultoverride = cfe({ type="boolean", value=checkgroupdefaultoverride(device_id), label="Group defaults have been overridden", readonly=true }) end if connected then provdb.databasedisconnect() end end) if not res and err then errtxt = err end end -- Save the device for later use local output = cfe({ type="group", value=retval, label="Provisioning Device Parameters", errtxt=errtxt }) if device_id and device_id ~= "" and not editable then saved_device_params[device_id] = duplicatestructure(output) end return output end -- This function is used by scripts, do not change prototype local set_device_params = function(params, editable, intransaction) -- Validate the settings local success = validateparam(params) local errtxt if success then local res, err = pcall(function() local connected = databaseconnect() success = validateparamcoded(params) local sql = "SELECT * FROM devices_to_classes WHERE device_id='"..provdb.escape(params.value.device_id.value).."' LIMIT 1" local tmp = getselectresponse(sql) if not tmp or #tmp == 0 then success = false errtxt = "Device does not exist" end if success then if not saved_device_params[params.value.device_id.value] then get_device_params(params.value.device_id.value) end local sql = "BEGIN TRANSACTION" if not intransaction then runsqlcommand(sql) end if not editable then -- Delete all values for this device (can't do this if only updating editable) sql = "DELETE FROM provisioning_values WHERE device_id='"..provdb.escape(params.value.device_id.value).."'" runsqlcommand(sql, true) end -- Loop through the groups and params for group,v in pairs(params.value) do if v.type == "group" then for name,param in pairs(v.value) do if editable then sql = "DELETE FROM provisioning_values WHERE device_id='"..provdb.escape(params.value.device_id.value).."' AND group_name='"..provdb.escape(group).."' AND param_id='"..provdb.escape(param.param_id).."'" runsqlcommand(sql, true) end if param.value ~= param.default then sql = "INSERT INTO provisioning_values VALUES('"..provdb.escape(params.value.device_id.value).."', '"..provdb.escape(group).."', '"..provdb.escape(param.param_id).."', '"..provdb.escape(tostring(param.value)).."')" runsqlcommand(sql, true) end end end end sql = "COMMIT" if not intransaction then runsqlcommand(sql) end local tmp = saved_device_params[params.value.device_id.value] local p = params if not editable then saved_device_params[params.value.device_id.value] = params else p = get_device_params(params.value.device_id.value) end callscript(updatedeviceparamsscriptfile, p, tmp) end if connected then provdb.databasedisconnect() end end) if not res and err then pcall(function() con:execute("ROLLBACK") end) success = false errtxt = err end end if not success then params.errtxt = errtxt or "Failed to save device parameters" end return params end local get_device_values = function(device_id) local retval = {} local errtxt if device_id and device_id ~= "" then local res, err = pcall(function() local connected = databaseconnect() local sql = "SELECT g.name AS group, p.name, p.type, CASE WHEN v.value IS NOT NULL THEN v.value WHEN g2p.value IS NOT NULL THEN g2p.value ELSE p.value END AS value ".. "FROM (devices_to_classes d2t JOIN provisioning_classes t USING(class_id) JOIN classes_to_param_groups t2g USING(class_id) JOIN provisioning_groups g USING(group_id) ".. "JOIN param_groups_to_params g2p USING(group_id) JOIN provisioning_params p USING(param_id)) LEFT JOIN provisioning_values v ON(d2t.device_id=v.device_id AND p.param_id=v.param_id AND g.name=v.group_name ) ".. "WHERE d2t.device_id='"..provdb.escape(device_id).."'" local tmp = getselectresponse(sql) -- Loop through the params and put them into the groups for i,p in ipairs(tmp) do if p.type == "boolean" then p.value = (p.value == "true") end if not retval[p.group] then retval[p.group] = {} end retval[p.group][p.name] = p.value end if connected then provdb.databasedisconnect() end end) if not res and err then errtxt = err end else errtxt = "Invalid device id" end return cfe({type="structure", value=retval, label="Parameter Values", errtxt=errtxt}) end -- These are the functions that may be called from within loaded Lua code -- The functions must be added after they're declared functions = { getselectresponse=getselectresponse, runsqlcommand=runsqlcommand, escape=provdb.escape, get_device=get_device, get_device_params=get_device_params, set_device_params=set_device_params, } -- ################################################################################ -- PUBLIC FUNCTIONS mymodule.list_templates = function() local retval = {} local errtxt -- Get the templates from the DB local res, err = pcall(function() local connected = databaseconnect() local sql = "SELECT value AS filename, label, seq FROM provisioning_options WHERE param_id = (SELECT param_id FROM provisioning_params WHERE name = 'template') ORDER BY seq ASC, label ASC, value ASC" retval = getselectresponse(sql) if connected then provdb.databasedisconnect() end end) if not res and err then errtxt = err end local reversed = {} for i,t in ipairs(retval) do reversed[t.filename] = i end -- Get the file stats for each template and add in any template files that aren't in the DB fs.create_directory(baseurl) for f in posix.files(baseurl) do local file = baseurl..f local details = fs.stat(file) if details.type == "regular" then if not reversed[file] then retval[#retval+1] = {filename=file} reversed[file] = #retval end local t = retval[reversed[file]] t.filesize = details.size t.mtime = details.mtime end end return cfe({ type="structure", value=retval, label="Templates", errtxt=errtxt }) end mymodule.get_template = function(self, clientdata) clientdata = clientdata or {} local filename = clientdata.filename local retval = {} if filename and not string.match(filename, "/") then filename = baseurl .. filename end retval.filename = cfe({value=filename or "", label="File Name", descr="Must be in "..baseurl, seq=1}) retval.label = cfe({label="Label", seq=2}) retval.seq = cfe({label="Sequence", seq=3}) local errtxt if filename and filename ~= "" then local res, err = pcall(function() local connected = databaseconnect() local sql = "SELECT * FROM provisioning_options WHERE param_id=(SELECT param_id FROM provisioning_params WHERE name = 'template') AND value='"..provdb.escape(filename).."' ORDER BY seq ASC, label ASC, value ASC" local tmp = getselectresponse(sql) if tmp and #tmp > 0 then for n,v in pairs(tmp[1]) do if n == "value" then retval.filename.value = v elseif retval[n] then retval[n].value = v end end end if connected then provdb.databasedisconnect() end end) if not res and err then errtxt = err end end local filedetails = modelfunctions.getfiledetails(retval.filename.value, function(filename) return validator.is_valid_filename(filename, baseurl) end) for i,n in ipairs({"filecontent", "filesize", "mtime"}) do retval[n] = filedetails.value[n] end return cfe({ type="group", value=retval, label="Provisioning Template", errtxt=errtxt }) end mymodule.create_template = function(self, template, action) return mymodule.update_template(self, template, action, true) end mymodule.update_template = function(self, template, action, create) local success = true local errtxt -- Validate the settings if template.value.filename and not string.match(template.value.filename.value, "/") then template.value.filename.value = baseurl .. template.value.filename.value end if not validator.is_valid_filename(template.value.filename.value, baseurl) then success = false template.value.filename.errtxt = "Invalid filename" end if template.value.label.value == "" then success = false template.value.label.errtxt = "Cannot be blank" end if not validator.is_integer(template.value.seq.value) then success = false template.value.seq.errtxt = "Must be an integer" end if success then local res, err = pcall(function() local connected = databaseconnect() local sql = "SELECT * FROM provisioning_options WHERE param_id=(SELECT param_id FROM provisioning_params WHERE name = 'template') AND value='"..provdb.escape(template.value.filename.value).."'" local tmp = getselectresponse(sql) if not create and (not tmp or #tmp == 0) then success = false errtxt = "Template does not exist" elseif create and #tmp > 0 then success = false errtxt = "Template already exists" end if success then if create then sql = "INSERT INTO provisioning_options VALUES((SELECT param_id FROM provisioning_params WHERE name='template'), '"..provdb.escape(template.value.label.value).."', '"..provdb.escape(template.value.filename.value).."', '"..provdb.escape(template.value.seq.value).."')" else sql = "UPDATE provisioning_options SET (label, seq) = ('"..provdb.escape(template.value.label.value).."', '"..provdb.escape(template.value.seq.value).."') WHERE param_id=(SELECT param_id FROM provisioning_params WHERE name = 'template') AND value='"..provdb.escape(template.value.filename.value).."'" end runsqlcommand(sql) fs.write_file(template.value.filename.value, string.gsub(format.dostounix(template.value.filecontent.value), "\n+$", "")) end if connected then provdb.databasedisconnect() end end) if not res and err then success = false errtxt = err end end if not success then if create then template.errtxt = errtxt or "Failed to create template" else template.errtxt = errtxt or "Failed to save template" end end return template end mymodule.get_delete_template = function(self, clientdata) clientdata = clientdata or {} local retval = {} retval.filename = cfe({ value=clientdata.filename or "", label="File Name" }) return cfe({ type="group", value=retval, label="Delete Template" }) end mymodule.delete_template = function(self, delreq) local filename = delreq.value.filename.value delreq.errtxt = "Failed to delete template" local res, err = pcall(function() local connected = databaseconnect() local sql = "SELECT * FROM provisioning_options WHERE param_id=(SELECT param_id FROM provisioning_params WHERE name = 'template') AND value='"..provdb.escape(filename).."'" local tmp = getselectresponse(sql) if #tmp == 0 then delreq.value.filename.errtxt = "Template does not exist" else -- Remove the template sql = "DELETE FROM provisioning_options WHERE param_id=(SELECT param_id FROM provisioning_params WHERE name = 'template') AND value='"..provdb.escape(filename).."'" runsqlcommand(sql) -- Delete the template file os.remove(filename) delreq.errtxt = nil end if connected then provdb.databasedisconnect() end end) if not res and err then delreq.errtxt = err end return delreq end mymodule.list_class_groups = function() local retval = {} local errtxt -- Get the groups from the DB local res, err = pcall(function() local connected = databaseconnect() local sql = "SELECT * FROM provisioning_class_groups ORDER BY seq ASC, label ASC" retval = getselectresponse(sql) if connected then provdb.databasedisconnect() end end) if not res and err then errtxt = err end return cfe({ type="structure", value=retval, label="Class Groups", errtxt=errtxt }) end mymodule.get_class_group = function(self, clientdata) clientdata = clientdata or {} local class_group_id = clientdata.class_group_id local retval = {} retval.class_group_id = cfe({value=class_group_id or "", label="Class Group ID", readonly=true, seq=1}) retval.name = cfe({label="Name", seq=2}) retval.label = cfe({label="Label", seq=3}) retval.seq = cfe({label="Sequence", seq=4}) local errtxt local res, err = pcall(function() local connected = databaseconnect() if class_group_id and class_group_id ~= "" then sql = "SELECT * FROM provisioning_class_groups WHERE class_group_id='"..provdb.escape(class_group_id).."'" tmp = getselectresponse(sql) if tmp and #tmp > 0 then for n,v in pairs(tmp[1]) do if retval[n] then retval[n].value = v end end end else retval.class_group_id = nil end if connected then provdb.databasedisconnect() end end) if not res and err then errtxt = err end return cfe({ type="group", value=retval, label="Provisioning Class Group", errtxt=errtxt }) end mymodule.create_class_group = function(self, group, action) return mymodule.update_class_group(self, group, action, true) end mymodule.update_class_group = function(self, group, action, create) local success = true local errtxt -- Validate the settings if group.value.name.value == "" then success = false group.value.name.errtxt = "Cannot be blank" elseif string.find(group.value.name.value, "%W") then success = false group.value.name.errtxt = "Must only contain alphanumeric characters" end if group.value.label.value == "" then success = false group.value.label.errtxt = "Cannot be blank" end if not validator.is_integer(group.value.seq.value) then success = false group.value.seq.errtxt = "Must be an integer" end if success then local res, err = pcall(function() local connected = databaseconnect() if not create then local sql = "SELECT * FROM provisioning_class_groups WHERE class_group_id='"..provdb.escape(group.value.class_group_id.value).."'" local tmp = getselectresponse(sql) if not tmp or #tmp == 0 then success = false errtxt = "Group does not exist" end end if success then local sql = "BEGIN TRANSACTION" runsqlcommand(sql) if create then sql = "INSERT INTO provisioning_class_groups VALUES(DEFAULT, '"..provdb.escape(group.value.name.value).."', '"..provdb.escape(group.value.label.value).."', '"..provdb.escape(group.value.seq.value).."')" runsqlcommand(sql, true) sql = "SELECT class_group_id FROM provisioning_class_groups WHERE label='"..provdb.escape(group.value.label.value).."'" local tmp = getselectresponse(sql, true) if tmp and #tmp>0 then group.value.class_group_id = cfe({value=tmp[1].class_group_id, label="Class Group ID", readonly=true, seq=1}) else error("Failed to insert class group", 0) end else sql = "UPDATE provisioning_class_groups SET (name, label, seq) = ('"..provdb.escape(group.value.name.value).."', '"..provdb.escape(group.value.label.value).."', '"..provdb.escape(group.value.seq.value).."') WHERE class_group_id='"..provdb.escape(group.value.class_group_id.value).."'" runsqlcommand(sql, true) end sql = "COMMIT" runsqlcommand(sql) end if connected then provdb.databasedisconnect() end end) if not res and err then pcall(function() con:execute("ROLLBACK") end) success = false errtxt = err end end if not success then if create then group.errtxt = errtxt or "Failed to create class group" else group.errtxt = errtxt or "Failed to save class group" end end return group end mymodule.get_delete_class_group = function(self, clientdata) clientdata = clientdata or {} local retval = {} retval.class_group_id = cfe({ value=clientdata.class_group_id or "", label="Class Group ID" }) return cfe({ type="group", value=retval, label="Delete Class Group" }) end mymodule.delete_class_group = function(self, delreq) local class_group_id = delreq.value.class_group_id.value delreq.errtxt = "Failed to delete class group" local res, err = pcall(function() local connected = databaseconnect() local sql = "SELECT * FROM provisioning_class_groups WHERE class_group_id='"..provdb.escape(class_group_id).."'" local tmp = getselectresponse(sql) if #tmp == 0 then delreq.value.class_group_id.errtxt = "Group does not exist" else sql = "DELETE FROM provisioning_class_groups WHERE class_group_id='"..provdb.escape(class_group_id).."'" runsqlcommand(sql) delreq.errtxt = nil end if connected then provdb.databasedisconnect() end end) if not res and err then delreq.errtxt = err end return delreq end mymodule.list_classes = function() local retval = {} local errtxt -- Get the classes from the DB local res, err = pcall(function() local connected = databaseconnect() local sql = "SELECT class_id, g.label AS group, g.name, c.label, c.seq FROM provisioning_classes c JOIN provisioning_class_groups g USING(class_group_id) ORDER BY g.seq ASC, g.label ASC, c.seq ASC, c.label ASC" retval = getselectresponse(sql) if connected then provdb.databasedisconnect() end end) if not res and err then errtxt = err end return cfe({ type="structure", value=retval, label="Classes", errtxt=errtxt }) end mymodule.get_class = function(self, clientdata) clientdata = clientdata or {} local class_id = clientdata.class_id local retval = {} retval.class_id = cfe({value=class_id or "", label="Class ID", readonly=true, seq=1}) retval.class_group_id = cfe({type="select", label="Class Group", option={}, seq=2}) retval.label = cfe({label="Label", seq=3}) retval.seq = cfe({label="Sequence", seq=4}) retval.groups = cfe({type="group", value={}, label="Parameter Groups", seq=5}) local errtxt local res, err = pcall(function() local groups = {} local connected = databaseconnect() if class_id and class_id ~= "" then local sql = "SELECT * FROM provisioning_classes WHERE class_id='"..provdb.escape(class_id).."'" local tmp = getselectresponse(sql) if tmp and #tmp > 0 then for n,v in pairs(tmp[1]) do if retval[n] then retval[n].value = v end end end -- Now, get the class-to-paramgroup mappings sql = "SELECT group_id FROM classes_to_param_groups WHERE class_id='"..provdb.escape(class_id).."'" tmp = getselectresponse(sql) for i,g in ipairs(tmp) do groups[g.group_id] = true end else retval.class_id = nil end -- Get the class_group_id options sql = "SELECT * from provisioning_class_groups ORDER BY seq ASC, label ASC" tmp = getselectresponse(sql) for i,g in ipairs(tmp) do retval.class_group_id.option[#retval.class_group_id.option + 1] = {value=g.class_group_id, label=g.label} end -- Finally, get the paramgroup options sql = "SELECT group_id, name, label FROM provisioning_groups ORDER BY seq ASC, name ASC" tmp = getselectresponse(sql) for i,g in ipairs(tmp) do if not retval.groups.value[g.name] then retval.groups.value[g.name] = cfe({type="select", label=g.name, option={{value="", label=""}}, seq=i}) end local group = retval.groups.value[g.name] group.option[#group.option + 1] = {value=g.group_id, label=g.label} if groups[g.group_id] then group.value = g.group_id end end if connected then provdb.databasedisconnect() end end) if not res and err then errtxt = err end return cfe({ type="group", value=retval, label="Provisioning Class", errtxt=errtxt }) end mymodule.create_class = function(self, class, action) return mymodule.update_class(self, class, action, true) end mymodule.update_class = function(self, class, action, create) local success = true local errtxt -- Validate the settings success = modelfunctions.validateselect(class.value.class_group_id) and success if class.value.label.value == "" then success = false class.value.label.errtxt = "Cannot be blank" end if not validator.is_integer(class.value.seq.value) then success = false class.value.seq.errtxt = "Must be an integer" end for n,g in pairs(class.value.groups.value) do success = modelfunctions.validateselect(g) and success end if success then local res, err = pcall(function() local connected = databaseconnect() if not create then local sql = "SELECT * FROM provisioning_classes WHERE class_id='"..provdb.escape(class.value.class_id.value).."'" local tmp = getselectresponse(sql) if not tmp or #tmp == 0 then success = false errtxt = "Class does not exist" end end if success then local sql = "BEGIN TRANSACTION" runsqlcommand(sql) if create then sql = "INSERT INTO provisioning_classes VALUES(DEFAULT, '"..provdb.escape(class.value.class_group_id.value).."', '"..provdb.escape(class.value.label.value).."', '"..provdb.escape(class.value.seq.value).."')" runsqlcommand(sql, true) sql = "SELECT class_id FROM provisioning_classes WHERE class_group_id='"..provdb.escape(class.value.class_group_id.value).."' AND label='"..provdb.escape(class.value.label.value).."'" local tmp = getselectresponse(sql, true) if tmp and #tmp>0 then class.value.class_id = cfe({value=tmp[1].class_id, label="Class ID", readonly=true, seq=1}) else error("Failed to insert class", 0) end else sql = "UPDATE provisioning_classes SET (class_group_id, label, seq) = ('"..provdb.escape(class.value.class_group_id.value).."', '"..provdb.escape(class.value.label.value).."', '"..provdb.escape(class.value.seq.value).."') WHERE class_id='"..provdb.escape(class.value.class_id.value).."'" runsqlcommand(sql, true) sql = "DELETE FROM classes_to_param_groups WHERE class_id='"..provdb.escape(class.value.class_id.value).."'" runsqlcommand(sql, true) end -- Insert the class to group entries for n,g in pairs(class.value.groups.value) do if g.value ~= "" then sql = "INSERT INTO classes_to_param_groups VALUES('"..provdb.escape(class.value.class_id.value).."', '"..provdb.escape(g.value).."')" runsqlcommand(sql, true) end end sql = "COMMIT" runsqlcommand(sql) end if connected then provdb.databasedisconnect() end end) if not res and err then pcall(function() con:execute("ROLLBACK") end) success = false errtxt = err end end if not success then if create then class.errtxt = errtxt or "Failed to create class" else class.errtxt = errtxt or "Failed to save class" end end return class end mymodule.get_delete_class = function(self, clientdata) clientdata = clientdata or {} local retval = {} retval.class_id = cfe({ value=clientdata.class_id or "", label="Class ID" }) return cfe({ type="group", value=retval, label="Delete Class" }) end mymodule.delete_class = function(self, delreq) local class_id = delreq.value.class_id.value delreq.errtxt = "Failed to delete class" local res, err = pcall(function() local connected = databaseconnect() local sql = "SELECT * FROM provisioning_classes WHERE class_id='"..provdb.escape(class_id).."'" local tmp = getselectresponse(sql) if #tmp == 0 then delreq.value.class_id.errtxt = "Class does not exist" else sql = "BEGIN TRANSACTION" runsqlcommand(sql) sql = "DELETE FROM classes_to_param_groups WHERE class_id='"..provdb.escape(class_id).."'" runsqlcommand(sql, true) sql = "DELETE FROM provisioning_classes WHERE class_id='"..provdb.escape(class_id).."'" runsqlcommand(sql, true) sql = "COMMIT" runsqlcommand(sql) delreq.errtxt = nil end if connected then provdb.databasedisconnect() end end) if not res and err then pcall(function() con:execute("ROLLBACK") end) delreq.errtxt = err end return delreq end mymodule.list_groups = function() local retval = {} local errtxt -- Get the groups from the DB local res, err = pcall(function() local connected = databaseconnect() local sql = "SELECT * FROM provisioning_groups ORDER BY seq ASC, name ASC, label ASC" retval = getselectresponse(sql) if connected then provdb.databasedisconnect() end end) if not res and err then errtxt = err end return cfe({ type="structure", value=retval, label="Parameter Groups", errtxt=errtxt }) end mymodule.get_group = function(self, clientdata) clientdata = clientdata or {} local group_id = clientdata.group_id local retval = {} retval.group_id = cfe({value=group_id or "", label="Group ID", readonly=true, seq=1}) retval.name = cfe({label="Name", seq=2}) retval.label = cfe({label="Label", seq=3}) retval.seq = cfe({label="Sequence", seq=4}) retval.params = cfe({type="multi", value={}, label="Parameters", option={}, descr="Each selected parameter will be included in the group", seq=5}) retval.editable = cfe({type="multi", value={}, label="Editable Parameters", option={}, descr="Each selected parameter will be user editable", seq=6}) retval.defaults = cfe({type="group", value={}, label="Parameter Defaults", seq=8}) local errtxt local res, err = pcall(function() local connected = databaseconnect() -- First, let's get all the parameters to set up the params.options and defaults local sql = "SELECT * FROM provisioning_params ORDER BY seq ASC, name ASC" local tmp = getselectresponse(sql) for i,p in ipairs(tmp) do retval.params.option[#retval.params.option + 1] = {value=p.param_id, label=p.label} retval.editable.option[#retval.editable.option + 1] = {value=p.param_id, label=p.label} p.seq = i if p.type == "select" then sql = "SELECT * FROM provisioning_options WHERE param_id='"..provdb.escape(p.param_id).."' ORDER BY seq ASC" p.option = getselectresponse(sql) or {} end if p.type == "boolean" then p.value = (p.value == "true") end p.default = p.value retval.defaults.value[p.param_id] = p end if group_id and group_id ~= "" then sql = "SELECT * FROM provisioning_groups WHERE group_id='"..provdb.escape(group_id).."'" tmp = getselectresponse(sql) if tmp and #tmp > 0 then for n,v in pairs(tmp[1]) do if retval[n] then retval[n].value = v end end end -- Now, get the paramgroup-to-param mappings sql = "SELECT * FROM param_groups_to_params WHERE group_id='"..provdb.escape(group_id).."'" tmp = getselectresponse(sql) for i,p in ipairs(tmp) do retval.params.value[#retval.params.value + 1] = p.param_id if (p.editable == "t") then retval.editable.value[#retval.editable.value + 1] = p.param_id end if p.value then if retval.defaults.value[p.param_id].type == "boolean" then retval.defaults.value[p.param_id].value = (p.value == "true") else retval.defaults.value[p.param_id].value = p.value end end end else retval.group_id = nil end if connected then provdb.databasedisconnect() end end) if not res and err then errtxt = err end return cfe({ type="group", value=retval, label="Provisioning Parameter Group", errtxt=errtxt }) end mymodule.create_group = function(self, group, action) return mymodule.update_group(self, group, action, true) end mymodule.update_group = function(self, group, action, create) local success = true local errtxt -- Validate the settings success = modelfunctions.validatemulti(group.value.params) and success success = modelfunctions.validatemulti(group.value.editable) and success if group.value.name.value == "" then success = false group.value.name.errtxt = "Cannot be blank" -- Group name cannot be device_id, name, or label or will mess up get_device_params elseif group.value.name.value == "device_id" or group.value.name.value == "name" or group.value.name.value == "label" then success = false group.value.name.errtxt = "Illegal name" elseif string.find(group.value.name.value, "%W") then success = false group.value.name.errtxt = "Must only contain alphanumeric characters" end if group.value.label.value == "" then success = false group.value.label.errtxt = "Cannot be blank" end if not validator.is_integer(group.value.seq.value) then success = false group.value.seq.errtxt = "Must be an integer" end -- Validate the param defaults for i,p in pairs(group.value.defaults.value) do success = validateparam(p, true) and success end if success then local res, err = pcall(function() local connected = databaseconnect() local devices = {} if not create then local sql = "SELECT * FROM provisioning_groups WHERE group_id='"..provdb.escape(group.value.group_id.value).."'" local tmp = getselectresponse(sql) if not tmp or #tmp == 0 then success = false errtxt = "Group does not exist" end -- Find all devices that have this group so we can notify on a param default value change sql = "SELECT d2t.device_id ".. "FROM (devices_to_classes d2t JOIN provisioning_classes t USING(class_id) JOIN classes_to_param_groups t2g USING (class_id)) ".. "WHERE t2g.group_id='"..provdb.escape(group.value.group_id.value).."'" devices = getselectresponse(sql) -- Make sure the current params are saved for each device for i,d in ipairs(devices) do if not saved_device_params[d.device_id] then get_device_params(d.device_id) end end end if success then local sql = "BEGIN TRANSACTION" runsqlcommand(sql) if create then sql = "INSERT INTO provisioning_groups VALUES(DEFAULT, '"..provdb.escape(group.value.name.value).."', '"..provdb.escape(group.value.label.value).."', '"..provdb.escape(group.value.seq.value).."')" runsqlcommand(sql, true) sql = "SELECT group_id FROM provisioning_groups WHERE name='"..provdb.escape(group.value.name.value).."' AND label='"..provdb.escape(group.value.label.value).."'" local tmp = getselectresponse(sql, true) if tmp and #tmp>0 then group.value.group_id = cfe({value=tmp[1].group_id, label="Group ID", readonly=true, seq=1}) else error("Failed to insert parameter group", 0) end else sql = "UPDATE provisioning_groups SET (name, label, seq) = ('"..provdb.escape(group.value.name.value).."', '"..provdb.escape(group.value.label.value).."', '"..provdb.escape(group.value.seq.value).."') WHERE group_id='"..provdb.escape(group.value.group_id.value).."'" runsqlcommand(sql, true) sql = "DELETE FROM param_groups_to_params WHERE group_id='"..provdb.escape(group.value.group_id.value).."'" runsqlcommand(sql, true) end -- Reverse the editable table for ease of use below local reverseeditable = {} for i,p in ipairs(group.value.editable.value) do reverseeditable[p] = i end -- Insert the group to param entries for i,p in ipairs(group.value.params.value) do sql = "INSERT INTO param_groups_to_params VALUES('"..provdb.escape(group.value.group_id.value).."', '"..provdb.escape(p).."', " if group.value.defaults.value[p].value ~= group.value.defaults.value[p].default then sql = sql.."'"..provdb.escape(tostring(group.value.defaults.value[p].value)).."'" else sql = sql.."null" end sql = sql..", '"..provdb.escape(tostring(reverseeditable[p] ~= nil)).."')" runsqlcommand(sql, true) end sql = "COMMIT" runsqlcommand(sql) -- Notify the devices that their params might have changed for i,d in ipairs(devices) do local tmp = saved_device_params[d.device_id] local p = get_device_params(d.device_id) callscript(updatedeviceparamsscriptfile, p, tmp) end end if connected then provdb.databasedisconnect() end end) if not res and err then pcall(function() con:execute("ROLLBACK") end) success = false errtxt = err end end if not success then if create then group.errtxt = errtxt or "Failed to create parameter group" else group.errtxt = errtxt or "Failed to save parameter group" end end return group end mymodule.get_delete_group = function(self, clientdata) clientdata = clientdata or {} local retval = {} retval.group_id = cfe({ value=clientdata.group_id or "", label="Group ID" }) return cfe({ type="group", value=retval, label="Delete Group" }) end mymodule.delete_group = function(self, delreq) local group_id = delreq.value.group_id.value delreq.errtxt = "Failed to delete parameter group" local res, err = pcall(function() local connected = databaseconnect() local sql = "SELECT * FROM provisioning_groups WHERE group_id='"..provdb.escape(group_id).."'" local tmp = getselectresponse(sql) if #tmp == 0 then delreq.value.group_id.errtxt = "Group does not exist" else sql = "BEGIN TRANSACTION" runsqlcommand(sql) sql = "DELETE FROM param_groups_to_params WHERE group_id='"..provdb.escape(group_id).."'" runsqlcommand(sql, true) sql = "DELETE FROM provisioning_groups WHERE group_id='"..provdb.escape(group_id).."'" runsqlcommand(sql, true) sql = "COMMIT" runsqlcommand(sql) delreq.errtxt = nil end if connected then provdb.databasedisconnect() end end) if not res and err then pcall(function() con:execute("ROLLBACK") end) delreq.errtxt = err end return delreq end mymodule.list_params = function() local retval = {} local errtxt -- Get the params from the DB local res, err = pcall(function() local connected = databaseconnect() local sql = "SELECT * FROM provisioning_params ORDER BY seq ASC, name ASC, label ASC" retval = getselectresponse(sql) if connected then provdb.databasedisconnect() end end) if not res and err then errtxt = err end return cfe({ type="structure", value=retval, label="Parameters", errtxt=errtxt }) end mymodule.get_param = function(self, clientdata) clientdata = clientdata or {} local param_id = clientdata.param_id local retval = {} retval.param_id = cfe({value=param_id or "", label="Param ID", readonly=true, seq=1}) retval.name = cfe({label="Name", seq=2}) retval.type = cfe({type="select", label="Type", option={"text", "boolean", "select"}, seq=3}) retval.label = cfe({label="Label", seq=4}) retval.descr = cfe({label="Description", seq=5}) retval.value = cfe({label="Default Value", descr="Warning, this value is not validated", seq=6}) retval.regexp = cfe({label="Regular Expression", descr="Lua regular expression for validating the text parameter value", seq=7}) retval.validate = cfe({type="longtext", label="Validation Code", descr="Lua code to validate the parameter value. Returns updated value and error text. Not used to validate group defaults.", seq=8}) retval.seq = cfe({label="Sequence", seq=9}) local errtxt local res, err = pcall(function() local connected = databaseconnect() if param_id and param_id ~= "" then sql = "SELECT * FROM provisioning_params WHERE param_id='"..provdb.escape(param_id).."'" tmp = getselectresponse(sql) if tmp and #tmp > 0 then for n,v in pairs(tmp[1]) do if retval[n] then retval[n].value = v end end end else retval.param_id = nil end if connected then provdb.databasedisconnect() end end) if not res and err then errtxt = err end return cfe({ type="group", value=retval, label="Provisioning Parameter", errtxt=errtxt }) end mymodule.create_param = function(self, param, action) return mymodule.update_param(self, param, action, true) end mymodule.update_param = function(self, param, action, create) local success = true local errtxt -- Validate the settings success = modelfunctions.validateselect(param.value.type) if param.value.name.value == "" then success = false param.value.name.errtxt = "Cannot be blank" end if param.value.label.value == "" then success = false param.value.label.errtxt = "Cannot be blank" end if not validator.is_integer(param.value.seq.value) then success = false param.value.seq.errtxt = "Must be an integer" end local s,e = validateluacode(param.value.validate.value) if not s then success = false param.value.validate.errtxt = "Invalid Lua code" if e and e ~= "" then param.value.validate.errtxt = param.value.validate.errtxt.."\n"..e end end if success then local res, err = pcall(function() local connected = databaseconnect() local devices = {} if not create then local sql = "SELECT * FROM provisioning_params WHERE param_id='"..provdb.escape(param.value.param_id.value).."'" local tmp = getselectresponse(sql) if not tmp or #tmp == 0 then success = false errtxt = "Param does not exist" end -- Find all devices that have this param so we can notify on a param default value change sql = "SELECT d2t.device_id FROM (devices_to_classes d2t JOIN provisioning_classes t USING(class_id) JOIN classes_to_param_groups t2g USING (class_id) ".. "JOIN provisioning_groups g USING(group_id) JOIN param_groups_to_params g2p USING(group_id))".. "WHERE g2p.param_id='"..provdb.escape(param.value.param_id.value).."'" devices = getselectresponse(sql) -- Make sure the current params are saved for each device for i,d in ipairs(devices) do if not saved_device_params[d.device_id] then get_device_params(d.device_id) end end end if success then local sql = "BEGIN TRANSACTION" runsqlcommand(sql) if create then sql = "INSERT INTO provisioning_params VALUES(DEFAULT, '"..provdb.escape(param.value.name.value).."', '"..provdb.escape(param.value.type.value).."', '"..provdb.escape(param.value.label.value).."', '"..provdb.escape(param.value.descr.value).."', '"..provdb.escape(param.value.value.value).."', '"..provdb.escape(param.value.seq.value).."', '"..provdb.escape(param.value.regexp.value).."', '"..provdb.escape(format.dostounix(param.value.validate.value)).."')" runsqlcommand(sql, true) sql = "SELECT param_id FROM provisioning_params WHERE name='"..provdb.escape(param.value.name.value).."' AND label='"..provdb.escape(param.value.label.value).."'" local tmp = getselectresponse(sql, true) if tmp and #tmp>0 then param.value.param_id = cfe({value=tmp[1].param_id, label="Param ID", readonly=true, seq=1}) else error("Failed to insert parameter", 0); end else sql = "UPDATE provisioning_params SET (name, type, label, descr, value, seq, regexp, validate) = ('"..provdb.escape(param.value.name.value).."', '"..provdb.escape(param.value.type.value).."', '"..provdb.escape(param.value.label.value).."', '"..provdb.escape(param.value.descr.value).."', '"..provdb.escape(param.value.value.value).."', '"..provdb.escape(param.value.seq.value).."', '"..provdb.escape(param.value.regexp.value).."', '"..provdb.escape(format.dostounix(param.value.validate.value)).."') WHERE param_id='"..provdb.escape(param.value.param_id.value).."'" runsqlcommand(sql, true) end sql = "COMMIT" runsqlcommand(sql) -- Notify the devices that their params might have changed for i,d in ipairs(devices) do local tmp = saved_device_params[d.device_id] local p = get_device_params(d.device_id) callscript(updatedeviceparamsscriptfile, p, tmp) end end if connected then provdb.databasedisconnect() end end) if not res and err then pcall(function() con:execute("ROLLBACK") end) success = false errtxt = err end end if not success then if create then param.errtxt = errtxt or "Failed to create parameter" else param.errtxt = errtxt or "Failed to save parameter" end end return param end mymodule.get_delete_param = function(self, clientdata) clientdata = clientdata or {} local retval = {} retval.param_id = cfe({ value=clientdata.param_id or "", label="Param ID" }) return cfe({ type="group", value=retval, label="Delete Param" }) end mymodule.delete_param = function(self, delreq) local param_id = delreq.value.param_id.value delreq.errtxt = "Failed to delete parameter" local res, err = pcall(function() local connected = databaseconnect() local sql = "SELECT * FROM provisioning_params WHERE param_id='"..provdb.escape(param_id).."'" local tmp = getselectresponse(sql) if #tmp == 0 then delreq.value.param_id.errtxt = "Parameter does not exist" else sql = "BEGIN TRANSACTION" runsqlcommand(sql) sql = "DELETE FROM provisioning_options WHERE param_id='"..provdb.escape(param_id).."'" runsqlcommand(sql, true) sql = "DELETE FROM provisioning_params WHERE param_id='"..provdb.escape(param_id).."'" runsqlcommand(sql, true) sql = "COMMIT" runsqlcommand(sql) delreq.errtxt = nil end if connected then provdb.databasedisconnect() end end) if not res and err then pcall(function() con:execute("ROLLBACK") end) delreq.errtxt = err end return delreq end mymodule.list_devices = function(self, clientdata) local retval = cfe({ type="group", value={}, label="Provisioning Devices" }) retval.value.page = cfe({ value=0, label="Page Number", descr="0 indicates ALL", key=true }) retval.value.pagesize = cfe({ value=10, label="Page Size", key=true }) retval.value.rowcount = cfe({ value=0, label="Row Count" }) -- orderby must be an array of tables with column name and direction retval.value.orderby = cfe({ type="structure", value={{column="device_id", direction="asc"}}, label="Order By", key=true }) -- filter is a table with a string filter for each column retval.value.filter = cfe({ type="structure", value={}, label="Filter", key=true }) self.handle_clientdata(retval, clientdata) retval.value.result = cfe({ type="structure", value={}, label="Provisioning Devices" }) retval.value.groups = cfe({ type="structure", value={}, label="Provisioning Class Groups" }) -- Process the incoming page data local page = tonumber(retval.value.page.value) or 0 retval.value.page.value = page local pagesize = tonumber(retval.value.pagesize.value) or 10 retval.value.pagesize.value = pagesize -- Get the devices from the DB local res, err = pcall(function() local connected = databaseconnect() -- First, get the columns / class_groups local sql = "SELECT * from provisioning_class_groups ORDER BY seq ASC" retval.value.groups.value = getselectresponse(sql) -- Set up the dynamic tables for order/filter and the sql statement for each column / class_group local columns = {device_id="device_id"} local selects = {"device_id"} local joins = {"devices_to_classes"} for i,g in ipairs(retval.value.groups.value) do columns[g.name] = provdb.escape(g.name)..".label" selects[#selects+1] = provdb.escape(g.name)..".label AS "..provdb.escape(g.name) joins[#joins+1] = "(SELECT device_id, c.label FROM devices_to_classes d2c JOIN provisioning_classes c USING(class_id) WHERE c.class_group_id='"..g.class_group_id.."') "..provdb.escape(g.name).." USING(device_id)" end -- Handle the sorting local orderby = {} local directions = {asc="ASC", desc="DESC", ASC="ASC", DESC="DESC"} for i,o in ipairs(retval.value.orderby.value) do if columns[o.column] and directions[o.direction] then orderby[#orderby+1] = columns[o.column].." "..directions[o.direction] end end if #orderby == 0 then orderby[#orderby+1] = "device_id ASC" end -- Handle the filtering local filter = {} for c,f in pairs(retval.value.filter.value) do if columns[c] and f ~= "" then filter[#filter+1] = columns[c].."~'"..provdb.escape(f).."'" end end -- Set up the joins and filtering first sql = " FROM "..table.concat(joins, " LEFT JOIN ") if #filter>0 then sql = sql.." WHERE "..table.concat(filter, " AND ") end -- If we're paginating, get the full count here if page > 0 then local count = getselectresponse("SELECT count(DISTINCT device_id)"..sql) retval.value.rowcount.value = count[1].count end -- Then we can finish the sql with the selects and sorting sql = "SELECT DISTINCT "..table.concat(selects, ", ")..sql sql = sql.." ORDER BY "..table.concat(orderby, ", ") if page > 0 then sql = sql.." LIMIT "..pagesize.." OFFSET "..(page - 1)*pagesize end retval.value.result.value = getselectresponse(sql) or {} if page <= 0 then retval.value.rowcount.value = #retval.value.result.value end if connected then provdb.databasedisconnect() end end) if not res and err then retval.errtxt = err end return retval end mymodule.get_existing_device = function(self, clientdata) clientdata = clientdata or {} return get_device(clientdata.device_id, false) end mymodule.get_new_device = function(self, clientdata) clientdata = clientdata or {} return get_device(clientdata.device_id, true) end mymodule.create_device = function(self, device, action) return mymodule.update_device(self, device, action, true) end mymodule.update_device = function(self, device, action, create, intransaction) local success = true local errtxt -- Validate the settings local something = false for n,c in pairs(device.value.classes.value) do success = modelfunctions.validateselect(c) and success if c.value ~= "" then something = true end end if not something then success = false end if success then local res, err = pcall(function() local connected = databaseconnect() if not create then local sql = "SELECT * FROM devices_to_classes WHERE device_id='"..provdb.escape(device.value.device_id.value).."' LIMIT 1" local tmp = getselectresponse(sql) if not tmp or #tmp == 0 then success = false device.value.device_id.errtxt = "Device does not exist" end end if success then if not saved_devices[device.value.device_id.value] then get_device(device.value.device_id.value) end if not saved_device_params[device.value.device_id.value] then get_device_params(device.value.device_id.value) end local sql = "BEGIN TRANSACTION" if not intransaction then runsqlcommand(sql) end if create then sql = "SELECT nextval('provisioning_device_seq')" local tmp = getselectresponse(sql, true) if tmp and #tmp>0 then device.value.device_id.value = tmp[1].nextval end end sql = "DELETE FROM devices_to_classes WHERE device_id='"..provdb.escape(device.value.device_id.value).."'" runsqlcommand(sql, true) -- Insert the device to class entries for n,c in pairs(device.value.classes.value) do if c.value ~= "" then sql = "INSERT INTO devices_to_classes VALUES('"..provdb.escape(device.value.device_id.value).."', '"..provdb.escape(c.value).."')" runsqlcommand(sql, true) end end sql = "COMMIT" if not intransaction then runsqlcommand(sql) end local s = saved_device_params[device.value.device_id.value] callscript(updatedevicescriptfile, device, saved_devices[device.value.device_id.value], get_device_params(device.value.device_id.value), s) saved_devices[device.value.device_id.value] = device end if connected then provdb.databasedisconnect() end end) if not res and err then pcall(function() con:execute("ROLLBACK") end) success = false errtxt = err end end if not success then if create then device.errtxt = errtxt or "Failed to create device" else device.errtxt = errtxt or "Failed to save device" end end return device end mymodule.get_delete_device = function(self, clientdata) clientdata = clientdata or {} local retval = {} retval.device_id = cfe({ value=clientdata.device_id or "", label="Device ID" }) return cfe({ type="group", value=retval, label="Delete Device" }) end mymodule.delete_device = function(self, delreq) local device_id = delreq.value.device_id.value delreq.errtxt = "Failed to delete device" local res, err = pcall(function() local connected = databaseconnect() local sql = "SELECT * FROM devices_to_classes WHERE device_id='"..provdb.escape(device_id).."' LIMIT 1" local tmp = getselectresponse(sql) if #tmp == 0 then delreq.value.device_id.errtxt = "Device does not exist" else if not saved_device_params[device_id] then get_device_params(device_id) end if not saved_devices[device_id] then get_device(device_id) end sql = "BEGIN TRANSACTION" runsqlcommand(sql) sql = "DELETE FROM provisioning_values WHERE device_id='"..provdb.escape(device_id).."'" runsqlcommand(sql, true) sql = "DELETE FROM devices_to_classes WHERE device_id='"..provdb.escape(device_id).."'" runsqlcommand(sql, true) sql = "COMMIT" runsqlcommand(sql) delreq.errtxt = nil callscript(deletedevicescriptfile, saved_devices[device_id], saved_device_params[device_id]) saved_devices[device_id] = nil saved_device_params[device_id] = nil end if connected then provdb.databasedisconnect() end end) if not res and err then pcall(function() con:execute("ROLLBACK") end) delreq.errtxt = err end return delreq end mymodule.get_editable_device_params = function(self, clientdata, action) clientdata = clientdata or {} return get_device_params(clientdata.device_id, true) end mymodule.get_all_device_params = function(self, clientdata, action) clientdata = clientdata or {} return get_device_params(clientdata.device_id, false) end mymodule.get_class_options = function(self, clientdata) clientdata = clientdata or {} local class_id = clientdata.class_id local retval = {} retval.class_id = cfe({value=class_id or "", label="Class ID", seq=1}) return cfe({ type="group", value=retval, label="Provisioning Class Values" }) end mymodule.get_class_values = function(self, retval) retval.errtxt = "Failed to find class" if not validator.is_integer(retval.value.class_id.value) then retval.value.class_id.errtxt = "Invalid class ID" else local res, err = pcall(function() local connected = databaseconnect() -- First, just check to see if class_id exists local sql = "SELECT * FROM provisioning_classes WHERE class_id='"..provdb.escape(retval.value.class_id.value).."'" local tmp = getselectresponse(sql) if tmp and #tmp > 0 then retval.errtxt = nil retval.value.label = cfe({ value=tmp[1].label or "", label="Label", seq=3}) retval.value.values = cfe({type="structure", value={}, label="Parameter Values"}) local sql = "SELECT g.name AS group, p.name, p.type, CASE WHEN g2p.value IS NOT NULL THEN g2p.value ELSE p.value END AS value ".. "FROM (provisioning_classes t JOIN classes_to_param_groups t2g USING(class_id) JOIN provisioning_groups g USING(group_id) ".. "JOIN param_groups_to_params g2p USING(group_id) JOIN provisioning_params p USING(param_id)) ".. "WHERE t.class_id='"..provdb.escape(retval.value.class_id.value).."'" local tmp = getselectresponse(sql) -- Loop through the params and put them into the groups for i,p in ipairs(tmp) do if p.type == "boolean" then p.value = (p.value == "true") end if not retval.value.values.value[p.group] then retval.value.values.value[p.group] = {} end retval.value.values.value[p.group][p.name] = p.value end else retval.value.class_id.errtxt = "Failed to find Class ID" end if connected then provdb.databasedisconnect() end end) if not res and err then retval.errtxt = err end end return retval end mymodule.set_editable_device_params = function(self, params) return set_device_params(params, true) end mymodule.set_all_device_params = function(self, params) return set_device_params(params, false) end mymodule.fetch_device_values = function(self, search) local res, err = pcall(function() local connected = databaseconnect() search = mymodule.search_device_values(self, search) if search.errtxt then -- Just return the error elseif #search.value.result.value == 0 then search.errtxt = "Device not found" elseif #search.value.result.value > 1 then search.errtxt = "Multiple devices found" else search.value.values = get_device_values(search.value.result.value[1].device_id) search.value.values.seq = 5 end if connected then provdb.databasedisconnect() end end) if not res and err then search.errtxt = err end search.label="Provisioning Device Parameter Values" return search end mymodule.get_search_options = function() local errtxt retval = {} retval.id = cfe({type="select", value="device_id", label="Parameter", option={"device_id"}, seq=1}) retval.comparison = cfe({type="select", value="=", label="Comparison", option={"=", "!=", "~", "!~", "~*", "!~*"}, seq=2}) retval.value = cfe({label="Parameter Value", descr="Parameter value or SQL regular expression", seq=3}) local res, err = pcall(function() local connected = databaseconnect() -- Get the group/parameter options local sql = "SELECT g.name AS group_name, p.name AS param_name FROM provisioning_groups g JOIN param_groups_to_params USING(group_id) JOIN provisioning_params p USING(param_id) GROUP BY g.name, p.name ORDER BY g.name ASC, p.name ASC" local tmp = getselectresponse(sql) local blankopt = {} local blankexists = {} for i,v in ipairs(tmp) do retval.id.option[#retval.id.option + 1] = v.group_name.."."..v.param_name if not blankexists[v.param_name] then blankopt[#blankopt+1] = "."..v.param_name blankexists[v.param_name] = true end end table.sort(blankopt) for i,o in ipairs(blankopt) do retval.id.option[#retval.id.option + 1] = o end if connected then provdb.databasedisconnect() end end) if not res and err then errtxt = err end return cfe({type="group", value=retval, label="Device Search", errtxt=errtxt}) end mymodule.search_device_values = function(self, search) local success = true success = modelfunctions.validateselect(search.value.id) and success success = modelfunctions.validateselect(search.value.comparison) and success if search.value.id.value == "device_id" then if string.match(search.value.comparison.value, "~") then success = false search.value.comparison.errtxt = "Cannot do regular expression comparison to device_id" end if not validator.is_integer(search.value.value.value) then success = false search.value.value.errtxt = "Must be an integer for comparison to device_id" end end if success then retval.result = cfe({type="structure", value={}, label="Devices", seq=4 }) local res, err = pcall(function() local connected = databaseconnect() -- Get the devices from the DB sql = "SELECT d2t.device_id, " local group, param = string.match(search.value.id.value, "([^%.]*)%.(.*)") if not group then sql = sql.."'"..provdb.escape(search.value.id.value).."' AS param, d2t."..provdb.escape(search.value.id.value).." AS value FROM devices_to_classes d2t WHERE d2t."..provdb.escape(search.value.id.value).. provdb.escape(search.value.comparison.value).."'"..provdb.escape(search.value.value.value).."' GROUP BY device_id" else sql = sql.."g.name as group, p.name as param, CASE WHEN v.value IS NOT NULL THEN v.value WHEN g2p.value IS NOT NULL THEN g2p.value ELSE p.value END as value FROM ".. "devices_to_classes d2t JOIN provisioning_classes t USING(class_id) JOIN classes_to_param_groups t2g USING(class_id) ".. "JOIN provisioning_groups g USING(group_id) JOIN param_groups_to_params g2p USING(group_id) JOIN provisioning_params p USING(param_id) ".. "LEFT JOIN provisioning_values v ON(d2t.device_id=v.device_id AND p.param_id=v.param_id AND g.name=v.group_name ) " if group and group ~= "" then sql = sql.."WHERE g.name='"..provdb.escape(group).."' AND" else sql = sql.."WHERE" end sql = sql.." p.name='"..provdb.escape(param).."' AND CASE WHEN v.value IS NOT NULL THEN v.value WHEN g2p.value IS NOT NULL THEN g2p.value ELSE p.value END"..provdb.escape(search.value.comparison.value).."'"..provdb.escape(search.value.value.value).."'" end sql = sql.." ORDER BY d2t.device_id ASC" search.value.result.value = getselectresponse(sql) if connected then provdb.databasedisconnect() end end) if not res and err then search.errtxt = err end else search.errtxt = "Search failed" end return search end mymodule.get_param_options = function(self, clientdata) clientdata = clientdata or {} local param_id = clientdata.param_id local retval = {} retval.param_id = cfe({value=param_id or "", label="Parameter ID", readonly=true, seq=0}) retval.name = cfe({label="Name", readonly=true, seq=1}) retval.label = cfe({label="Label", readonly=true, seq=2}) retval.options = cfe({type="list", value={}, label="Value Options", descr="Ordered list of options where each option is made up of 'value' or 'value,label'", seq=3}) local errtxt = "Cannot find parameter" if param_id and param_id ~= "" then local res, err = pcall(function() local connected = databaseconnect() -- First, just check to see if param_id exists local sql = "SELECT * FROM provisioning_params WHERE param_id='"..provdb.escape(param_id).."'" local tmp = getselectresponse(sql) if tmp and #tmp > 0 then errtxt = nil retval.name.value = tmp[1].name retval.label.value = tmp[1].label -- Next, get all of the param options sql = "SELECT * FROM provisioning_options WHERE param_id='"..provdb.escape(param_id).."' ORDER BY seq ASC" local tmp = getselectresponse(sql) or {} for i,t in ipairs(tmp) do retval.options.value[#retval.options.value + 1] = t.value..","..t.label end end if connected then provdb.databasedisconnect() end end) if not res and err then errtxt = err end end return cfe({ type="group", value=retval, label="Provisioning Parameter Options", errtxt=errtxt }) end mymodule.set_param_options = function(self, options) local success = true local errtxt -- Validate the settings --FIXME if success then local res, err = pcall(function() local connected = databaseconnect() local sql = "SELECT * FROM provisioning_params WHERE param_id='"..provdb.escape(options.value.param_id.value).."'" local tmp = getselectresponse(sql) if not tmp or #tmp == 0 then success = false errtxt = "Parameter does not exist" end if success then local sql = "BEGIN TRANSACTION" runsqlcommand(sql) -- Delete all options for this device sql = "DELETE FROM provisioning_options WHERE param_id='"..provdb.escape(options.value.param_id.value).."'" runsqlcommand(sql, true) -- Loop through the options for i,o in ipairs(options.value.options.value) do local v,l = string.match(o, "^%s*([^,]*),%s*(.*%S?)%s*$") if v then v = string.match(v, "^(.*%S?)%s*$") else v = string.match(o, "^%s*(.*%S?)%s*$") l = v end sql = "INSERT INTO provisioning_options VALUES('"..provdb.escape(options.value.param_id.value).."', '"..provdb.escape(l).."', '"..provdb.escape(v).."', '"..i.."')" runsqlcommand(sql, true) end sql = "COMMIT" runsqlcommand(sql) end if connected then provdb.databasedisconnect() end end) if not res and err then pcall(function() con:execute("ROLLBACK") end) success = false errtxt = err end end if not success then options.errtxt = errtxt or "Failed to save options" end return options end function mymodule.get_filedetails(self, clientdata) clientdata = clientdata or {} return modelfunctions.getfiledetails(clientdata.filename, scriptfiles) end function mymodule.update_filedetails(self, filedetails) return modelfunctions.setfiledetails(self, filedetails, scriptfiles, validatefiledetails) end function mymodule.list_files() local retval = {} for i,file in ipairs(scriptfiles) do local details = fs.stat(file) or {} details.filename = file table.insert(retval, details) end table.sort(retval, function(a,b) return a.filename < b.filename end) return cfe({ type="structure", value=retval, label="Provisioning Script Files" }) end function mymodule.get_database() local devices = cfe({ type="boolean", value=false, label="Include Device Data" }) return cfe({ type="group", value={devices=devices}, label="Provisioning Database Dump" }) end function mymodule.dump_database(self, db) local res, err = pcall(function() local connected = databaseconnect() local lines = {} local sql = "SELECT name, label, seq FROM provisioning_class_groups ORDER BY name, label" local tmp = getselectresponse(sql) for i,t in ipairs(tmp) do lines[#lines+1] = "INSERT INTO provisioning_class_groups VALUES(default, '"..provdb.escape(t.name).."', '"..provdb.escape(t.label).."', '"..provdb.escape(t.seq).."');" end sql = "SELECT g.name AS group, c.label, c.seq FROM provisioning_classes c JOIN provisioning_class_groups g USING(class_group_id) ORDER BY g.name, c.label" tmp = getselectresponse(sql) for i,t in ipairs(tmp) do lines[#lines+1] = "INSERT INTO provisioning_classes VALUES(default, (SELECT class_group_id FROM provisioning_class_groups WHERE name='"..provdb.escape(t.group).."'), '"..provdb.escape(t.label).."', '"..provdb.escape(t.seq).."');" end sql = "SELECT name, label, seq FROM provisioning_groups ORDER BY name, label" tmp = getselectresponse(sql) for i,t in ipairs(tmp) do lines[#lines+1] = "INSERT INTO provisioning_groups VALUES(default, '"..provdb.escape(t.name).."', '"..provdb.escape(t.label).."', '"..provdb.escape(t.seq).."');" end sql = "SELECT name, type, label, descr, value, seq, regexp, validate FROM provisioning_params ORDER BY name" tmp = getselectresponse(sql) for i,t in ipairs(tmp) do lines[#lines+1] = "INSERT INTO provisioning_params VALUES(default, '"..provdb.escape(t.name).."', '"..provdb.escape(t.type).."', '"..provdb.escape(t.label).."', '"..provdb.escape(t.descr).."', '"..provdb.escape(t.value).."', '"..provdb.escape(t.seq).."', '"..provdb.escape(t.regexp).."', '"..provdb.escape(t.validate).."');" end sql = "SELECT p.name AS param, o.label, o.value, o.seq FROM provisioning_options o JOIN provisioning_params p USING(param_id) ORDER BY p.name, o.seq, o.label" tmp = getselectresponse(sql) for i,t in ipairs(tmp) do lines[#lines+1] = "INSERT INTO provisioning_options VALUES((SELECT param_id FROM provisioning_params WHERE name='"..provdb.escape(t.param).."'), '"..provdb.escape(t.label).."', '"..provdb.escape(t.value).."', '"..provdb.escape(t.seq).."');" end sql = "SELECT c.label AS class, g.label AS group FROM provisioning_classes c JOIN classes_to_param_groups USING(class_id) JOIN provisioning_groups g USING(group_id) ORDER BY c.label, g.label" tmp = getselectresponse(sql) for i,t in ipairs(tmp) do lines[#lines+1] = "INSERT INTO classes_to_param_groups VALUES((SELECT class_id FROM provisioning_classes WHERE label='"..provdb.escape(t.class).."'), (SELECT group_id FROM provisioning_groups WHERE label='"..provdb.escape(t.group).."'));" end sql = "SELECT p.name AS param, g.label AS group, t.value, t.editable FROM provisioning_params p JOIN param_groups_to_params t USING(param_id) JOIN provisioning_groups g USING(group_id) ORDER BY p.name, g.label" tmp = getselectresponse(sql) for i,t in ipairs(tmp) do lines[#lines+1] = "INSERT INTO param_groups_to_params VALUES((SELECT group_id FROM provisioning_groups WHERE label='"..provdb.escape(t.group).."'), (SELECT param_id FROM provisioning_params WHERE name='"..provdb.escape(t.param).."'), '"..provdb.escape(t.value).."', '"..provdb.escape(t.editable).."');" end if db.value.devices and db.value.devices.value then sql = "SELECT device_id FROM devices_to_classes GROUP BY device_id ORDER BY device_id ASC" devices = getselectresponse(sql) for i,d in ipairs(devices) do sql = "SELECT label FROM devices_to_classes JOIN provisioning_classes USING(class_id) WHERE device_id='"..provdb.escape(d.device_id).."'" tmp = getselectresponse(sql) for j,t in ipairs(tmp) do lines[#lines+1] = "INSERT INTO devices_to_classes VALUES("..provdb.escape(i)..", (SELECT class_id FROM provisioning_classes WHERE label='"..provdb.escape(t.label).."'));" end sql = "SELECT group_name, p.name AS param, v.value FROM provisioning_values v JOIN provisioning_params p USING(param_id) WHERE device_id='"..provdb.escape(d.device_id).."'" tmp = getselectresponse(sql) for j,t in ipairs(tmp) do lines[#lines+1] = "INSERT INTO provisioning_values VALUES("..provdb.escape(i)..", '"..provdb.escape(t.group_name).."', (SELECT param_id FROM provisioning_params WHERE name='"..provdb.escape(t.param).."'), '"..provdb.escape(t.value).."');" end end lines[#lines+1] = "SELECT pg_catalog.setval('provisioning_device_seq', "..provdb.escape(#devices)..", true);" end db.value.data = cfe({ type="longtext", value=table.concat(lines, "\n") or "", label="Database Values" }) if connected then provdb.databasedisconnect() end end) if not res and err then db.errtxt = err end return db end function mymodule.get_file(self, clientdata) clientdata = clientdata or {} local mac = clientdata.mac local ip = clientdata.ip local agent = clientdata.agent local result = cfe() -- Error if there's no mac if not mac or mac == "" then result.errtxt = "No MAC Address" return result end local res, err = pcall(function() local connected = databaseconnect() -- Add the device to the table of requests local sql = "SELECT * FROM provisioning_requests WHERE mac='"..provdb.escape(string.upper(mac)).."'" local requests = getselectresponse(sql) if requests and #requests > 0 then sql = "UPDATE provisioning_requests SET ip='"..provdb.escape(ip).."', agent='"..provdb.escape(agent).."', date=now() WHERE mac='"..provdb.escape(string.upper(mac)).."'" else sql = "INSERT INTO provisioning_requests VALUES('"..provdb.escape(string.upper(mac)).."', '"..provdb.escape(ip).."', '"..provdb.escape(agent).."', now())" end runsqlcommand(sql) -- Now, let's see if this device exists result = mymodule.get_search_options() result.value.id.value = "device.mac" result.value.value.value = string.upper(mac) result = mymodule.fetch_device_values(self, result) -- If can't find the device, return a file with default settings if #result.value.result.value == 0 then -- Determine which class to use (need a class that specifies a template) local c = mymodule.list_classes() local class = callscript(determineclassscriptfile, agent, c) if class then local options = mymodule.get_class_options(self, {}) options.value.class_id.value = class result = mymodule.get_class_values(self, options) else result.errtxt = "Unknown device" end end if connected then provdb.databasedisconnect() end end) if not res and err then result.errtxt = err end return result end function mymodule.put_file(self, clientdata) clientdata = clientdata or {} local mac = clientdata.mac local data = clientdata.data local retval = cfe({ label="PUT Data" }) -- Error if there's no mac if not mac or mac == "" then retval.errtxt = "No MAC Address" return retval end local res, err = pcall(function() local connected = databaseconnect() -- Now, let's see if this device exists local search = mymodule.get_search_options() search.value.id.value = "device.mac" search.value.value.value = string.upper(mac) search = mymodule.search_device_values(self, search) if search.errtxt then retval.errtxt = search.errtxt elseif #search.value.result.value == 0 then retval.errtxt = "Device not found" elseif #search.value.result.value > 1 then retval.errtxt = "Multiple devices found" else retval.value, retval.errtxt = callscript(processputscriptfile, mac, data, search.value.result.value[1].device_id) -- If the script doesn't exist, allow the write retval.value = retval.value or data end if connected then provdb.databasedisconnect() end end) if not res and err then retval.errtxt = err end return retval end mymodule.list_requests = function(self, clientdata) local retval = cfe({ type="group", value={}, label="Requests" }) retval.value.page = cfe({ value=0, label="Page Number", descr="0 indicates ALL", key=true }) retval.value.pagesize = cfe({ value=10, label="Page Size", key=true }) retval.value.rowcount = cfe({ value=0, label="Row Count" }) -- orderby must be an array of tables with column name and direction retval.value.orderby = cfe({ type="structure", value={{column="date", direction="asc"}}, label="Order By", key=true }) -- filter is a table with a string filter for each column retval.value.filter = cfe({ type="structure", value={}, label="Filter", key=true }) self.handle_clientdata(retval, clientdata) retval.value.result = cfe({ type="structure", value={}, label="Requests" }) -- Process the incoming page data local page = tonumber(retval.value.page.value) or 0 retval.value.page.value = page local pagesize = tonumber(retval.value.pagesize.value) or 10 retval.value.pagesize.value = pagesize local orderby = {} local columns = {device_id="c.device_id", agent="r.agent", date="r.date", ip="r.ip", mac="r.mac"} local directions = {asc="ASC", desc="DESC", ASC="ASC", DESC="DESC"} for i,o in ipairs(retval.value.orderby.value) do if columns[o.column] and directions[o.direction] then orderby[#orderby+1] = columns[o.column].." "..directions[o.direction] end end if #orderby == 0 then orderby[#orderby+1] = "r.date DESC" end local res, err = pcall(function() local connected = databaseconnect() local filter = {} columns.data = nil -- Cannot regex filter based on date because of the timestamp type for c,f in pairs(retval.value.filter.value) do if columns[c] and f ~= "" then if c == "device_id" then if tonumber(f) then filter[#filter+1] = columns[c].."='"..provdb.escape(f).."'" else filter[#filter+1] = columns[c].." IS NULL" end else filter[#filter+1] = columns[c].."~'"..provdb.escape(f).."'" end end end local sql = " FROM (provisioning_requests r LEFT JOIN (SELECT v.device_id, v.value FROM (provisioning_values v JOIN provisioning_params p USING(param_id)) WHERE p.name='mac') c ON r.mac=c.value)" if #filter>0 then sql = sql.." WHERE "..table.concat(filter, " AND ") end if page > 0 then local count = getselectresponse("SELECT count(*)"..sql) retval.value.rowcount.value = count[1].count end sql = sql.." ORDER BY "..table.concat(orderby, ", ") if page > 0 then sql = sql.." LIMIT "..pagesize.." OFFSET "..(page - 1)*pagesize end retval.value.result.value = getselectresponse("SELECT r.*, c.device_id"..sql) or {} if page <= 0 then retval.value.rowcount.value = #retval.value.result.value end if connected then provdb.databasedisconnect() end end) if not res and err then retval.errtxt = err end return retval end mymodule.get_delete_request = function(self, clientdata) clientdata = clientdata or {} local retval = {} retval.mac = cfe({ value=clientdata.mac or "", label="MAC Address" }) return cfe({ type="group", value=retval, label="Delete Request" }) end mymodule.delete_request = function(self, delreq) local mac = delreq.value.mac.value delreq.errtxt = "Failed to delete request" local res, err = pcall(function() local connected = databaseconnect() local sql = "SELECT * FROM provisioning_requests WHERE mac='"..provdb.escape(string.upper(mac)).."'" local tmp = getselectresponse(sql) if #tmp == 0 then delreq.value.mac.errtxt = "Request does not exist" else -- Remove the request sql = "DELETE FROM provisioning_requests WHERE mac='"..provdb.escape(string.upper(mac)).."'" runsqlcommand(sql) delreq.errtxt = nil end if connected then provdb.databasedisconnect() end end) if not res and err then delreq.errtxt = err end return delreq end mymodule.get_request = function(self, clientdata) clientdata = clientdata or {} local mac = clientdata.mac local retval = {} retval.mac = cfe({ value=mac or "", label="MAC Address" }) return cfe({ type="group", value=retval, label="Create Device from Request" }) end mymodule.create_from_request = function(self, request) success = false local res, err = pcall(function() local connected = databaseconnect() local sql = "SELECT * FROM provisioning_requests WHERE mac='"..provdb.escape(string.upper(request.value.mac.value)).."'" local tmp = getselectresponse(sql) if #tmp == 0 then request.value.mac.errtxt = "Request does not exist" else -- Determine which class to use (need a class that specifies a template) local c = mymodule.list_classes() local class, group = callscript(determineclassscriptfile, tmp[1].agent, c) -- Create the device local device = get_device(nil, true) if class and group and device.value.classes.value[group] then device.value.classes.value[group].value = class device = mymodule.create_device(self, device) if device.errtxt then request.errtxt = {device.errtxt} for n,v in pairs(device.value) do if v.errtxt then request.errtxt[#request.errtxt + 1] = v.errtxt end end request.errtxt = table.concat(request.errtxt, "\n") else local params = get_device_params(device.value.device_id.value) -- Set the MAC Address if params.value.device and params.value.device.value.mac then params.value.device.value.mac.value = string.upper(request.value.mac.value) params = set_device_params(params) end if params.errtxt then request.errtxt = params:print_errtxt() local req = mymodule.get_delete_device(self, {device_id = device.value.device_id.value}) mymodule.delete_device(self, req) else success = true request.value.device_id = device.value.device_id end end else request.errtxt = "Failed to create device - could not determine class" end end if connected then provdb.databasedisconnect() end end) if not res and err then request.errtxt = err end if not success and not request.errtxt then request.errtxt = "Failed to create device" end return request end mymodule.get_bulk_create_request = function(self, clientdata) local retval = {} retval.bulkdevicedata = cfe({ type="list", value={}, label="Bulk device list", descr="List of devices as comma-separated fields (first row must contain column headers)" }) return cfe({ type="group", value=retval, label="Create Multiple Devices" }) end mymodule.bulk_create_devices = function(self, devicelist) -- To allow uploading a file, check the bulkdevicedata format -- Haserl will pass a temporary file name if a file is uploaded if #devicelist.value.bulkdevicedata.value == 1 and string.find(devicelist.value.bulkdevicedata.value[1], "^/tmp/[^.]+$") and fs.is_file(devicelist.value.bulkdevicedata.value[1]) then devicelist.value.bulkdevicedata.value = fs.read_file_as_array(devicelist.value.bulkdevicedata.value[1]) or {} end local res, err = pcall(function() local connected = databaseconnect() local groups = mymodule.list_class_groups() local headers = format.string_to_table(devicelist.value.bulkdevicedata.value[1], ",") local reverseheaders = {} for i,h in ipairs(headers) do reverseheaders[h] = i end runsqlcommand("BEGIN TRANSACTION") for i=2,#devicelist.value.bulkdevicedata.value do local values = format.string_to_table(devicelist.value.bulkdevicedata.value[i], ",") -- Create the device local device = get_device(nil, true) for name,class in pairs(device.value.classes.value) do if reverseheaders[name] then local value = values[reverseheaders[name]] -- Allow the class value to be the class_id number or a unique string local found = false if tonumber(value) then for k,c in ipairs(class.option) do if tonumber(value) == tonumber(c.value) then found = true class.value = value break end end end if not found then -- Check for string match for k,c in ipairs(class.option) do if string.find(c.label, format.escapemagiccharacters(value)) then if found then error("Ambiguous "..name.." class in line "..i) end found = c.value end end if found then class.value = found else error("Invalid "..name.." class in line "..i) end end end end device = mymodule.update_device(self, device, "Create", true, true) if device.errtxt then local err = {"Failed to create device in line "..i} err[#err+1] = device.errtxt for n,v in pairs(device.value) do if v.errtxt then err[#err + 1] = v.errtxt end end error(table.concat(err, "\n")) end -- Set the params local params = get_device_params(device.value.device_id.value, false) for j,h in ipairs(headers) do if values[j] and string.find(h, "%.") then local class,param = string.match(h, "^([^.]+)%.(.*)") if params.value[class] and params.value[class].value[param] then if params.value[class].value[param].type == "select" then -- Allow the select value to be the value or a unique substring of the label local found = false for k,o in ipairs(params.value[class].value[param].option) do if values[j] == o.value then found = o.value break elseif values[j] ~= "" and string.find(o.label, format.escapemagiccharacters(values[j])) then if found then error("Ambiguous "..h.." value in line "..i) end found = o.value end end if found then params.value[class].value[param].value = found else error("Invalid "..h.." value in line "..i) end elseif params.value[class].value[param].type == "boolean" then params.value[class].value[param].value = ("true" == string.lower(values[j])) else params.value[class].value[param].value = values[j] end end end end params = set_device_params(params, false, true) if params.errtxt then local err = {"Failed to create device in line "..i} err[#err+1] = params.errtxt for cname,cvalue in pairs(params.value) do if type(cvalue.value) == "table" then for pname,pvalue in pairs(cvalue.value) do if pvalue.errtxt then err[#err + 1] = cname.."."..pname..": "..pvalue.errtxt end end end end error(table.concat(err, "\n")) end end runsqlcommand("COMMIT") if connected then provdb.databasedisconnect() end end) if not res and err then pcall(function() runsqlcommand("ROLLBACK") end) devicelist.errtxt = err end return devicelist end mymodule.get_bulk_dump_request = function(self, clientdata) local retval = {} return cfe({ type="group", value=retval, label="Dump Multiple Devices" }) end mymodule.bulk_dump_devices = function(self, dumprequest) dumprequest.value.devices = cfe({type="raw", value={}, label="devices.csv"}) local res, err = pcall(function() local connected = databaseconnect() local classes = {} local reverseclasses = {} local devicevalues = {} local columns = {} local reversecolumns = {} local devices = mymodule.list_devices(self, {}) for i,d in ipairs(devices.value.result.value) do -- Capture all of the classes for class in pairs(d) do if class ~= "device_id" and not reverseclasses[class] then classes[#classes+1] = class reverseclasses[class] = #classes end end -- Capture all of the params devicevalues[i] = get_device_values(d.device_id) for class,value in pairs(devicevalues[i].value) do for param in pairs(value) do if not reversecolumns[class.."."..param] then columns[#columns+1] = class.."."..param reversecolumns[class.."."..param] = #columns end end end end table.sort(classes) table.sort(columns) local device = {} for i,c in ipairs(classes) do device[#device+1] = c end for i,p in ipairs(columns) do device[#device+1] = p end dumprequest.value.devices.value[#dumprequest.value.devices.value+1] = table.concat(device, ",") for i,d in ipairs(devicevalues) do device = {} for j,c in ipairs(classes) do device[#device+1] = devices.value.result.value[i][c] or "" end for j,p in ipairs(columns) do local class,param = string.match(p, "([^.]*)%.(.*)") if d.value[class] and d.value[class][param] ~= nil then device[#device+1] = tostring(d.value[class][param]) else device[#device+1] = "" end end dumprequest.value.devices.value[#dumprequest.value.devices.value+1] = table.concat(device, ",") end if connected then provdb.databasedisconnect() end end) if not res and err then dumprequest.errtxt = err end dumprequest.value.devices.value = table.concat(dumprequest.value.devices.value, "\n") return dumprequest end return mymodule