#!/usr/bin/env lua -- If you need filesystem access, use nixio.fs local fs = require "nixio.fs" -- LuCI JSON is used for checking the arguments and converting tables to JSON. local jsonc = require "luci.jsonc" -- Nixio provides syslog functionality local nixio = require "nixio" -- To access /etc/config files, use the uci module local UCI = require "luci.model.uci" -- Slight overkill, but leaving room to do log_info etcetera. local function log_to_syslog(level, message) nixio.syslog(level, message) end local function log_error(message) log_to_syslog("err", "[luci.example]: " .. message) end local function using_uci_directly(section) -- Rather than parse files in /etc/config, you can rely on the -- luci.model.uci module. local uci = UCI.cursor() -- https://openwrt.github.io/luci/api/modules/luci.model.uci.html local config_name = uci:get("example", section) uci.unload("example") if not config_name then local msg = "'" .. section .. "' not found in /etc/config/example" -- Send the log message to syslog so it can be found with logread log_error(msg) -- Convert a lua table into JSON notation and print to stdout -- .stringify() is equivalent to cjson's .encode() print(jsonc.stringify({uci_error = msg})) -- Indicate failure in the return code os.exit(1) end return config_name end -- The methods table defines all of the APIs to expose to rpcd. -- rpcd will execute this Lua file with the 'list' argument to discover the -- method names that can be presented over ubus, as well as any arguments -- those methods take. local methods = { -- How to call this API: -- echo '{"section": "first"}' | lua /usr/libexec/rpcd/luci.example call get_uci_value -- echo '{"section": "does_not_exist"}' | lua /usr/libexec/rpcd/luci.example call get_uci_value get_uci_value = { -- Args are specified as a table, where the argument type is specified by example -- The value is not used as a default. args = {section = "a_string"}, -- A special key of 'call' points to a function definition for execution. call = function(args) -- A table for the result. local r = {} r.result = jsonc.stringify({ example_section = using_uci_directly(args.section) }) -- The 'call' handler will refer to '.code', but also defaults if not found. r.code = 0 -- Return the table object; the call handler will access the attributes -- of the table. return r end }, -- How to call this API: -- echo '{}' | lua /usr/libexec/rpcd/luci.example call get_sample1 -- ubus call luci.example get_sample1 get_sample1 = { call = function() local r = {} -- This structure does not map well to a JSONMap in the LuCI form setup. -- It can be rendered as a table easily enough with loops. r.result = jsonc.stringify({ num_cats = 1, num_dogs = 2, num_parakeets = 4, is_this_real = false, not_found = nil }) return r end }, -- How to call this API: -- echo '{}' | lua /usr/libexec/rpcd/luci.example call get_sample2 -- ubus call luci.example get_sample2 get_sample2 = { call = function() local r = {} -- This is the structural data that JSONMap will work with in the JS file local data = { option_one = { name = "Some string value", value = "A value string", parakeets = {"one", "two", "three"}, }, option_two = { name = "Another string value", value = "And another value", parakeets = {3, 4, 5}, } } r.result = jsonc.stringify(data) return r end } } local function parseInput() -- Input parsing - the RPC daemon calls the Lua script and -- sends input to it via stdin, not as an argument on the CLI. -- Thus, any testing via the lua interpreter needs to be in the form -- echo '{jsondata}' | lua /usr/libexec/rpcd/script call method_name local parse = jsonc.new() local done, err while true do local chunk = io.read(4096) if not chunk then break elseif not done and not err then done, err = parse:parse(chunk) end end if not done then print(jsonc.stringify({ error = err or "Incomplete input for argument parsing" })) os.exit(1) end return parse:get() end local function validateArgs(func, uargs) -- Validates that arguments picked out by parseInput actually match -- up to the arguments expected by the function being called. local method = methods[func] if not method then print(jsonc.stringify({error = "Method not found in methods table"})) os.exit(1) end -- Lua has no length operator for tables, so iterate to get the count -- of the keys. local n = 0 for _, _ in pairs(uargs) do n = n + 1 end -- If the method defines an args table (so empty tables are not allowed), -- and there were no args, then give a useful error message about that. if method.args and n == 0 then print(jsonc.stringify({ error = "Received empty arguments for " .. func .. " but it requires " .. jsonc.stringify(method.args) })) os.exit(1) end uargs.ubus_rpc_session = nil local margs = method.args or {} for k, v in pairs(uargs) do if margs[k] == nil or (v ~= nil and type(v) ~= type(margs[k])) then print(jsonc.stringify({ error = "Invalid argument '" .. k .. "' for " .. func .. " it requires " .. jsonc.stringify(method.args) })) os.exit(1) end end return method end if arg[1] == "list" then -- When rpcd starts up, it executes all scripts in /usr/libexec/rpcd -- passing 'list' as the first argument. This block of code examines -- all of the entries in the methods table, and looks for an attribute -- called 'args' to see if there are arguments for the method. -- -- The end result is a JSON struct like -- { -- "api_name": {}, -- "api2_name": {"host": "some_string"} -- } -- -- Which will be converted by ubus to -- "api_name":{} -- "api2_name":{"host":"String"} local _, rv = nil, {} for _, method in pairs(methods) do rv[_] = method.args or {} end print((jsonc.stringify(rv):gsub(":%[%]", ":{}"))) elseif arg[1] == "call" then -- rpcd will execute the Lua script with a first argument of 'call', -- a second argument of the method name, and a third argument that's -- stringified JSON. -- -- To debug your script, it's probably easiest to start with direct -- execution, as calling via ubus will hide execution errors. For example: -- echo '{}' | lua /usr/libexec/rpcd/luci.example call get_sample2 -- -- or -- -- echo '{"section": "firstf"}' | /usr/libexec/rpcd/luci.example call get_uci_value -- -- See https://openwrt.org/docs/techref/ubus for more details on using -- ubus to call your RPC script (which is what LuCI will be doing). local args = parseInput() local method = validateArgs(arg[2], args) local run = method.call(args) -- Use the result from the table which we know to be JSON already. -- Anything printed on stdout is sent via rpcd to the caller. Use -- the syslog functions, or logging to a file, if you need debug -- logs. print(run.result) -- And exit with the code supplied. os.exit(run.code or 0) elseif arg[1] == "help" then local helptext = [[ Usage: To see what methods are exported by this script: lua luci.example list To call a method that has no arguments: echo '{}' | lua luci.example call method_name To call a method that takes arguments: echo '{"valid": "json", "argument": "value"}' | lua luci.example call method_name To call this script via ubus: ubus call luci.example method_name '{"valid": "json", "argument": "value"}' ]] print(helptext) end