diff options
Diffstat (limited to 'applications/luci-app-example/root')
4 files changed, 280 insertions, 8 deletions
diff --git a/applications/luci-app-example/root/etc/uci-defaults/80_example b/applications/luci-app-example/root/etc/uci-defaults/80_example index 529e6b0bd4..f896943169 100644 --- a/applications/luci-app-example/root/etc/uci-defaults/80_example +++ b/applications/luci-app-example/root/etc/uci-defaults/80_example @@ -3,6 +3,7 @@ touch /etc/config/example uci set example.first=first uci set example.second=second +uci set example.third=third uci commit return 0 diff --git a/applications/luci-app-example/root/usr/libexec/rpcd/luci.example b/applications/luci-app-example/root/usr/libexec/rpcd/luci.example new file mode 100755 index 0000000000..e3e5f8795e --- /dev/null +++ b/applications/luci-app-example/root/usr/libexec/rpcd/luci.example @@ -0,0 +1,248 @@ +#!/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 diff --git a/applications/luci-app-example/root/usr/share/luci/menu.d/luci-app-example.json b/applications/luci-app-example/root/usr/share/luci/menu.d/luci-app-example.json index 7b4772328a..4c82dd988a 100644 --- a/applications/luci-app-example/root/usr/share/luci/menu.d/luci-app-example.json +++ b/applications/luci-app-example/root/usr/share/luci/menu.d/luci-app-example.json @@ -26,5 +26,32 @@ "type": "view", "path": "example/htmlview" } + }, + + "admin/example/rpc-array": { + "title": "RPC Array Example", + "order": 3, + "action": { + "type": "view", + "path": "example/rpc" + } + }, + + "admin/example/rpc-jsonmap-table": { + "title": "RPC JSONMap Table Example", + "order": 4, + "action": { + "type": "view", + "path": "example/rpc-jsonmap-tablesection" + } + }, + + "admin/example/rpc-jsonmap-typed": { + "title": "RPC JSONMap Typed Example", + "order": 5, + "action": { + "type": "view", + "path": "example/rpc-jsonmap-typedsection" + } } } diff --git a/applications/luci-app-example/root/usr/share/rpcd/acl.d/luci-app-example.json b/applications/luci-app-example/root/usr/share/rpcd/acl.d/luci-app-example.json index 136f9aed55..b6849ffe6c 100644 --- a/applications/luci-app-example/root/usr/share/rpcd/acl.d/luci-app-example.json +++ b/applications/luci-app-example/root/usr/share/rpcd/acl.d/luci-app-example.json @@ -1,10 +1,11 @@ { "luci-app-example": { - "description": "Grant UCI access to LuCI app example", + "description": "Grant UCI and RPC access to LuCI app example", "read": { "ubus": { - "uci": [ - "get" + "luci.example": [ + "get_sample1", + "get_sample2" ] }, "uci": [ @@ -12,11 +13,6 @@ ] }, "write": { - "ubus": { - "uci": [ - "set", "commit" - ] - }, "uci": [ "example" ] |