刚结束一个急活,主要是整理某个 OpenWRT 路由的设置界面,网页服务主要用的是 Nginx,网页用的是 BackBone 和 jQuery 配合,后端设置服务主要用的是 Lua(由 Nginx 代理)调用 OpenWRT 的 UCI 和 ubus。一开始我以为只需要前端稍微调整下就行了,后来发现后边跟着的 Lua 得一起整,顺带补了不少 OpenWRT 的基础知识,下边简单梳(还)理(债)……

UCI

基础知识

UCI 是“Unified Configuration Interface”(统一配置界面)的缩写,是 OpenWrt 系统的核心配置框架,它的主要作用是整合系统里不同的设置项,并提供一个统一的接口。OpenWrt 系统配置文件默认被集中放在了 /etc/config 这里(当然也可以放在其它地方),这些 UCI 文件有自己特殊的语法,比如一个典型的无线配置可能是:

config wifi-device 'radio0'
    option type 'mac80211'
    option channel 'auto'
    option hwmode '11g'
    option path 'platform/qca953x_wmac'
    option htmode 'HT20'
    option disabled '0'

config wifi-iface
option ifname 'wlan0'
option device 'radio0'
option network 'lan'
option mode 'ap'
option encryption 'none'
option ssid 'TestSSID'


这里以 config 开头的行代表了一个 config 节点,其格式为:
config 'section-type' 'section'
section-type 处的值是节点类型,而 section 则是节点名称。另外,config 节点允许匿名节点的存在(意即直接跳过'section',就像第二个的 config 节点那样,wifi-iface只是节点类型而不是节点名,这里要注意),引号在 UCI 文件中也不是必须的,严格来讲只有值里带有空格或制表符时才需要使用,使用时也要注意,其必须成对出现才有效(比如一对单引号或者一对双引号,交叉使用会导致语法错误)。
以 option 开头的是选项,格式为:
option 'key' 'value'
这是比较典型的 key-value 格式,就不再赘述了。除此之外,还有种 list 列表选项,被用来描述形如数组类的设置,格式与 option 非常相似:
list 'list-key' 'list-value'
如果 list-key 相同的话,那么这实际上就是个数组式的设置项,举个栗子,system 设置里的 NTP:


config timeserver 'ntp'
option enabled '1'
option enable_server '0'
list server '0.openwrt.pool.ntp.org'
list server '1.openwrt.pool.ntp.org'
list server '2.openwrt.pool.ntp.org'
list server '3.openwrt.pool.ntp.org'

这里的 NTP Server 设置实际上就是个数组。


UCI 的调用


在 OpenWRT 系统里调用 UCI 一般有两种方法,通过命令行或者是调用 Lua API。这里首先说命令行。
OpenWRT 官方文档里提到,使用awk、grep等命令来解析Openwrt的配置文件是低效和不明智的做法,并建议在类似的场景下,应该优先使用命令行形式调用。
UCI 命令行语法为(在命令行下直接输入 uci 即可看到):


用法: uci [<options>] <command> [<arguments>]

命令:
batch
export [<config>]
import [<config>]
changes [<config>]
commit [<config>]
add <config> <section-type>
add_list <config>.<section>.<option>=<string>
show [<config>[.<section>[.<option>]]]
get <config>.<section>[.<option>]
set <config>.<section>[.<option>]=<value>
delete <config>[.<section[.<option>]]
rename <config>.<section>[.<option>]=<name>
revert <config>[.<section>[.<option>]]

参数:
-c <path> 设置用于存储配置文件的文件夹 (默认位于: /etc/config)
-d <str> 使用'uci show'命令时,为 list 类型的值设置分隔符
-f <file> 使用指定的 <file> 作为输入,而不是默认的 stdin
-m 导入时,合并数据到现有的设置中
-n 导出时,命名匿名节 (默认)
-N 不要命名匿名节
-p <path> 添加一个配置文件的搜索路径
-P <path> 添加一个配置文件的搜索路径并将其作为默认设置
-q 安静默认 (不打印错误信息)
-s 强制使用严格模式 (在解析出现错误时停止,默认)
-S 关闭严格模式
-X 在'show'命令上显示匿名节点ID (如果有的话)


平时(命令行下)常用的主要是 showgetsetchangescommit 这几个。
使用 UCI 时,需要特别注意下它的读写规则:UCI 在读取时,会首先读取内存中的缓存,而后才是文件;而写入则与此相反,增删改都是在操作缓存,需要手动提交才会将设置项写入到系统中。所以,在编写路由设置系统时,最后的提交操作是切不可忘的一步。
还有一种调用 UCI 的方法,是使用 Lua,文末的参考内容[3]中有详细的 API 列表(记得在开头用 local uci = require "los.uci".cursor() 语句引入)。
在使用 Lua 调用时,有个需要注意的点是匿名节点,比如上文中的无线配置里,有个 wifi-iface 类型的匿名节点,在命令行里使用 uci show wireless 可以看到:


wireless.radio1=wifi-device
wireless.radio1.type='mac80211'
wireless.radio1.channel='auto'
wireless.radio1.hwmode='11g'
wireless.radio1.path='platform/qca953x_wmac'
wireless.radio1.htmode='HT20'
wireless.radio1.disabled='0'
wireless.@wifi-iface[0]=wifi-iface
wireless.@wifi-iface[0].ifname='wlan0'
wireless.@wifi-iface[0].device='mt7620'
wireless.@wifi-iface[0].network='lan'
wireless.@wifi-iface[0].mode='ap'
wireless.@wifi-iface[0].encryption='none'
wireless.@wifi-iface[0].ssid='TestSSID'

这里可以看到很多键名类似 @wifi-iface[0] 的设置项,这就是匿名节点的设置项了。如果在命令行里加入 -X 参数变成 uci -X show wireless,则可以看到:


wireless.radio1=wifi-device
wireless.radio1.type='mac80211'
wireless.radio1.channel='auto'
wireless.radio1.hwmode='11g'
wireless.radio1.path='platform/qca953x_wmac'
wireless.radio1.htmode='HT20'
wireless.radio1.disabled='0'
wireless.cfg043579=wifi-iface
wireless.cfg043579.ifname='wlan1'
wireless.cfg043579.device='radio1'
wireless.cfg043579.network='lan'
wireless.cfg043579.mode='ap'
wireless.cfg043579.encryption='none'
wireless.cfg043579.ssid='TestSSID'

这时 @wifi-iface[0] 变成了 cfg043579,这才是这个匿名节点真实的引用名(系统自动生成的)。
而同样的,在撰写相对应的 Lua 语句时,也不能写成:


uci:get("wireless", "@wifi-iface[0]", "ssid", "NewSSID")

虽然可以在命令行执行 uci set wireless.@wifi-iface[0].ssid='NewSSID',但是在 Lua 上这么写系统是不会鸟你的(更何况还有个隐性的问题,是设置被改动过后,匿名节点的位置有可能会变,比如会跑到 @wifi-iface[1] 去,这可能会发生在拥有多个匿名节点的配置文件里)。所以这个时候,就需要使用 uci:foreach 去遍历某个设置类型的所有设置节点(注:返回 false 终止遍历),在遍历出的内容里,有几个特殊的、键名以英文字符 . 开头的成员:



  • [.index]: 设置节点的索引

  • [.name]: 设置节点的名称(即真实的引用名,cfg043579 这种)

  • [.type]: 设置节点的类型(如 wifi-iface

  • [.anonymous]: 指示该设置节点是否匿名


这样,通过遍历所有项目并筛选符合条件的配置项,将 [.name] 中的内容缓存下来,就可以用:


uci:get("wireless", "cfg043579", "ssid", "NewSSID")

这种方法去调用了。
这里放个自己写的用于遍历无线设置的函数(双频设备,每个频段只有一个信号,通过设备 ID 来识别):


function getWirelessInfo()
local wifiConfig = {}
uci:foreach(
"wireless",
"wifi-iface",
function(s)
if s.device "mt7620" then
if not wifiConfig.mt7620 then
wifiConfig["mt7620"] = {}
end
local key = ""
if s.key then
key = s.key
end
wifiConfig.mt7620 = {
name = s[".name"],
ssid = s.ssid,
ency = s.encryption,
pass = key
}
elseif s.device "mt7612" then
if not wifiConfig.mt7612 then
wifiConfig["mt7612"] = {}
end
local key = ""
if s.key then
key = s.key
end
wifiConfig.mt7612 = {
name = s[".name"],
ssid = s.ssid,
ency = s.encryption,
pass = key
}
end
end
)
return wifiConfig
end

不过,在实践中,我认为最有效的手段是将匿名节点转化成普通的具名节点,这样 Lua 就可以直接调用,比写挨个遍历内容的逻辑要简单也清晰的多。


下边再说说 ubus。


ubus


ubus 即是 OpenWrt micro bus 架构,是 OpenWrt 为了提供守护进程和应用程序间的通讯而开发的项目。简单来说,想获取系统运行的一些状态,是可以用 ubus 来查看的,而且相比用 UCI 查询,由于 ubus 获取的直接是系统信息而不是设置项,所以可以避免由于错误配置带来的配置项与系统状态不符合的问题。也是因为这个原因,我推荐读取设置(状态)时用 ubus,写入设置时用 UCI。
当然 ubus 也并不是没有问题,目前比较通用的说法是,在数据内容超过 60k 时不建议用,另外如果有多线程、或者逻辑上有递归时也不建议用(指令发出以后,接受到的信息可能是另一条指令的返回内容)。


ubus 的调用


同 UCI 类似,调用 ubus 也分为命令行方式与 Lua 调用方式。而与 UCI 将设置文件命名为包(package)不同的是,ubus 将其调度单位称为“命名空间”(namespace),系统后台会默认驻留一个名为 ubusd 的守护进程,使用友好的 JSON 格式进行交互。
在命令行中输入 ubus list 就可以看到所有通过RPC服务器注册的命名空间:


dhcp
hostapd.wlan0
hostapd.wlan1
log
network
network.device
network.interface
network.interface.lan
network.interface.loopback
network.interface.wan
network.interface.wan6
network.wireless
service
session
system
uci

加个参数变成 ubus -v list,就可以详细列出这些命名空间所提供的方法了。调用方法用 call 关键字,比如,查看系统 WiFi 状态就可以用:


ubus call network.wireless status '{}'

(参数一定要带上,即使为空。格式为 JSON)


除此以外,还有:



  • 获取系统信息(上线时间、内存用量、SWAP信息等)
    ubus call system info '{}'


  • 获取设备信息(设备型号、固件版本等)
    ubus call system board '{}'


  • 获取 WiFi 上已连接的客户端
    ubus call hostapd.wlan0 get_clients '{}'


  • 获取路由物理设备信息(如 MAC 型号、工作状态等)
    ubus call network.device status '{"name":"eth0"}'
    等等。
    除了命令行直接调用外,ubus 也可以使用 Lua 调用,由于没有 UCI 那劳什子匿名节点的问题,所以直接用
    local ubus = require "ubus"
    引入,在调用前用
    local conn = ubus.connect()
    连接服务,在调用后用
    conn:close()
    关闭就好。
    比如我自己写的一段从 ubus 上拿 WiFi 信息的函数:
    local function getWirelessStatus()
    local conn = ubus.connect()
    if not conn then
    error("Failed to connect to ubusd")
    end
    local info = {}
    local status = conn:call("network.wireless", "status", {})
    for k, v in pairs(status) do
    info[k] = v
    end
    conn:close()
    return info
    end





参考内容



  1. OpenWRT官网 - UCI系统

  2. OpenWRT官网 - UCI技术参考资料

  3. LuaDoc - luci.model.uci (英)

  4. OpenWRT官网 - ubus