luci-app-example: rewrite RPC side using ucode

Signed-off-by: George Sapkin <george@sapk.in>
This commit is contained in:
George Sapkin
2025-02-03 15:55:55 +02:00
committed by Paul Donald
parent 0f743ad0f2
commit f32d9204a7
7 changed files with 64 additions and 266 deletions
@@ -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)
@@ -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)
@@ -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', {}, [
@@ -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
@@ -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
@@ -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 };
+7 -8
View File
@@ -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.