From f32d9204a7f5351ac708d839313526e0c9dba7a8 Mon Sep 17 00:00:00 2001 From: George Sapkin Date: Mon, 3 Feb 2025 15:55:55 +0200 Subject: [PATCH] luci-app-example: rewrite RPC side using ucode Signed-off-by: George Sapkin --- .../view/example/rpc-jsonmap-tablesection.js | 3 +- .../view/example/rpc-jsonmap-typedsection.js | 3 +- .../luci-static/resources/view/example/rpc.js | 7 +- .../root/etc/uci-defaults/80_example | 6 +- .../root/usr/libexec/rpcd/luci.example | 248 ------------------ .../root/usr/share/rpcd/ucode/example.uc | 48 ++++ applications/luci-app-example/structure.md | 15 +- 7 files changed, 64 insertions(+), 266 deletions(-) delete mode 100755 applications/luci-app-example/root/usr/libexec/rpcd/luci.example create mode 100755 applications/luci-app-example/root/usr/share/rpcd/ucode/example.uc diff --git a/applications/luci-app-example/htdocs/luci-static/resources/view/example/rpc-jsonmap-tablesection.js b/applications/luci-app-example/htdocs/luci-static/resources/view/example/rpc-jsonmap-tablesection.js index eb0c965e69..69fe8c8a2a 100644 --- a/applications/luci-app-example/htdocs/luci-static/resources/view/example/rpc-jsonmap-tablesection.js +++ b/applications/luci-app-example/htdocs/luci-static/resources/view/example/rpc-jsonmap-tablesection.js @@ -9,8 +9,7 @@ listed by the shell command $ ubus list -Custom scripts can be placed in /usr/libexec/rpcd, and must emit JSON. The name of the file -in that directory will be the value for the object key in the declared map. +Custom ucode scripts can be placed in /usr/share/rpcd/ucode, and must emit JSON. Permissions to make these calls must be granted in /usr/share/rpcd/acl.d via a file named the same as the application package name (luci-app-example) diff --git a/applications/luci-app-example/htdocs/luci-static/resources/view/example/rpc-jsonmap-typedsection.js b/applications/luci-app-example/htdocs/luci-static/resources/view/example/rpc-jsonmap-typedsection.js index 26ab5eee84..a0731408f0 100644 --- a/applications/luci-app-example/htdocs/luci-static/resources/view/example/rpc-jsonmap-typedsection.js +++ b/applications/luci-app-example/htdocs/luci-static/resources/view/example/rpc-jsonmap-typedsection.js @@ -9,8 +9,7 @@ listed by the shell command $ ubus list -Custom scripts can be placed in /usr/libexec/rpcd, and must emit JSON. The name of the file -in that directory will be the value for the object key in the declared map. +Custom ucode scripts can be placed in /usr/share/rpcd/ucode, and must emit JSON. Permissions to make these calls must be granted in /usr/share/rpcd/acl.d via a file named the same as the application package name (luci-app-example) diff --git a/applications/luci-app-example/htdocs/luci-static/resources/view/example/rpc.js b/applications/luci-app-example/htdocs/luci-static/resources/view/example/rpc.js index d04e1e66db..3b8f82129c 100644 --- a/applications/luci-app-example/htdocs/luci-static/resources/view/example/rpc.js +++ b/applications/luci-app-example/htdocs/luci-static/resources/view/example/rpc.js @@ -9,8 +9,7 @@ listed by the shell command $ ubus list -Custom scripts can be placed in /usr/libexec/rpcd, and must emit JSON. The name of the file -in that directory will be the value for the object key in the declared map. +Custom ucode scripts can be placed in /usr/share/rpcd/ucode, and must emit JSON. Permissions to make these calls must be granted in /usr/share/rpcd/acl.d via a file named the same as the application package name (luci-app-example) @@ -104,9 +103,7 @@ return view.extend({ // return is used to modify the DOM that the browser shows. render: function (data) { // data[0] will be the result from load_sample1 - var sample1 = data[0] || {}; - // data[1] will be the result from load_sample_yaml - var sample_yaml = data[1] || {}; + const sample1 = data[0] || {}; // Render the tables as individual sections. return E('div', {}, [ 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 f896943169..aad59eb845 100644 --- a/applications/luci-app-example/root/etc/uci-defaults/80_example +++ b/applications/luci-app-example/root/etc/uci-defaults/80_example @@ -4,6 +4,10 @@ touch /etc/config/example uci set example.first=first uci set example.second=second uci set example.third=third -uci commit +uci set example.animals=animals +uci set example.animals.num_cats=1 +uci set example.animals.num_dogs=2 +uci set example.animals.num_parakeets=4 +uci commit example 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 deleted file mode 100755 index e3e5f8795e..0000000000 --- a/applications/luci-app-example/root/usr/libexec/rpcd/luci.example +++ /dev/null @@ -1,248 +0,0 @@ -#!/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/rpcd/ucode/example.uc b/applications/luci-app-example/root/usr/share/rpcd/ucode/example.uc new file mode 100755 index 0000000000..d6e21fcfeb --- /dev/null +++ b/applications/luci-app-example/root/usr/share/rpcd/ucode/example.uc @@ -0,0 +1,48 @@ +#!/usr/bin/env ucode + +'use strict'; + +import { cursor } from 'uci'; + +// Rather than parse files in /etc/config, we can use `cursor`. +const uci = cursor(); + +const methods = { + get_sample1: { + call: function() { + const num_cats = uci.get('example', 'animals', 'num_cats'); + const num_dogs = uci.get('example', 'animals', 'num_dogs'); + const num_parakeets = uci.get('example', 'animals', 'num_parakeets'); + const result = { + num_cats, + num_dogs, + num_parakeets, + is_this_real: false, + not_found: null, + }; + + uci.unload(); + return result; + } + }, + + get_sample2: { + call: function() { + const result = { + 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], + }, + }; + return result; + } + } +}; + +return { 'luci.example': methods }; diff --git a/applications/luci-app-example/structure.md b/applications/luci-app-example/structure.md index 6d3b67e820..0c53772fdf 100644 --- a/applications/luci-app-example/structure.md +++ b/applications/luci-app-example/structure.md @@ -9,6 +9,8 @@ │ └── example │ ├── form.js │ ├── htmlview.js +│ ├── rpc-jsonmap-tablesection.js +│ ├── rpc-jsonmap-typedsection.js │ └── rpc.js ├── Makefile ├── po @@ -21,16 +23,15 @@ │ └── uci-defaults │ └── 80_example └── usr - ├── libexec - │ └── rpcd - │ └── luci.example └── share ├── luci │ └── menu.d │ └── luci-app-example.json └── rpcd - └── acl.d - └── luci-app-example.json + ├── acl.d + │ └── luci-app-example.json + └── ucode + └── example.uc ``` @@ -68,9 +69,7 @@ LuCI apps do not have to have any additional files such as Lua scripts or UCI de ### Installing additional files -Any additional files needed by this application should be placed in `root/` using the directory tree that applies. This example application needs a RPCd script to be installed, so it places a file in `root/usr/libexec/rpcd/` and calls it `luci.example`. Scripts must have their execution bit set, and committed to the git repository with the bit set. - -This example application also installs a file in `/etc/` by putting it in `root/etc/luci.example.yaml`. +Any additional files needed by this application should be placed in `root/` using the directory tree that applies. This example application needs a ucode RPCd script to be installed, so it places a file in `root/usr/share/rpcd/ucode` and called `example.uc`. The OpenWrt packaging system will install these files automatically. -- 2.30.2