开发OpenWrt路由器上LuCI的模块+++
发布日期:2021-06-29 02:32:41 浏览次数:2 分类:技术文章

本文共 12754 字,大约阅读时间需要 42 分钟。

原文 

【题外话】

学校里最近改造了校园网,要求必须用iNode验证,万幸的是路由器能刷OpenWrt,并且OpenWrt上有好多iNode认证的开源项目,比如 (以下简称njit-client)就非常好用。虽然程序写的好用,但是配置起来还是稍微麻烦一些的,大家通常的方法是在/etc/init.d下写启动脚本,把用户名、密码什么的都直接填进去,但毕竟配置起来不方便,同时日后修改起来也不便。好在用Lua为LuCI写配置模块很简单,索性就自己做了一个,现在把开发的流程写一下,方便初学的同学去做。为njit-client做好的Web配置界面也已经开源,地址: ,或者直接下载编译好(不限平台)的文件:

【文章索引】

【一、LuCI配置界面开发的框架】

LuCI是OpenWrt上的Web管理界面,LuCI采用了MVC三层架构,同时其使用Lua脚本开发,所以开发LuCI的配置界面不需要编辑任何的Html代码,除非想自己单独去创建网页(View层),否则我们基本上只需要修改Model层就可以了。官方也有一个如何去创建模块的说明文档,虽然写的比较晦涩:

要为LuCI增加一个新模块,首先需要创建两个文件,一个位于Controller(/usr/lib/lua/luci/controller/)下,定义模块的入口;另一个位于Model(/usr/lib/lua/luci/model/cbi/)下,为配置模块实际的代码。

首先我们定义模块的入口,在/usr/lib/lua/luci/controller/下创建一个lua文件,类似如下:

module("luci.controller.控制器名", package.seeall)function index()        entry(路径, 调用目标, _("显示名称"), 显示顺序)        end

第一行说明了程序和模块的名称,比如在controller/目录创建一个mymodule.lua,那么就可以写成“luci.controller.mymodule”,如果你的程序比较多,可能分为好几个模块,那么可以在controller下再常见一个子目录,比如controller/myapp/,那么就可以写成“luci.controller.myapp.mymodule”。

接下来的entry表示添加一个新的模块入口,官方给出了entry的定义,其中后两项都是可以为空的:

entry(path, target, title=nil, order=nil)

第一项是访问的路径,不过路径是按字符串数组给定的,比如路径按如下方式写“{"click", "here", "now"}”,那么就可以在浏览器里访问“http://192.168.1.1/cgi-bin/luci/click/here/now”来访问这个脚本。而通常我们希望为管理员菜单添加脚本,那么我们需要按如下方式编写“{"admin", "一级菜单名", "菜单项名"}”,系统会自动在对应的菜单中生成菜单项。比如想在“网络”菜单下创建一个菜单项,那么一级菜单名可以写为“network”。

第二项为调用目标,调用目标分为三种,分别是执行指定方法(Action)、访问指定页面(Views)以及调用CBI Module。

  • 第一种可以直接调用指定的函数,比如点击菜单项就直接重启路由器等等,比如写为“call("function_name")”,然后在lua文件下编写名为function_name的函数就可以调用了。
  • 第二种可以访问指定的页面,比如写为“template("myapp/mymodule")”就可以调用/usr/lib/lua/luci/view/myapp/mymodule.htm文件了。
  • 而如果要编写配置页面,那么使用第三种方法无非是最方便的,比如写为“cbi("myapp/mymodule")”就可以调用/usr/lib/lua/luci/model/cbi/myapp/mymodule.lua文件了。

而title和order无非是针对管理员菜单来的,可以参考其他的lua文件来决定编写的内容。

这里我们创建/usr/lib/lua/luci/controller/njitclient.lua文件,定义我们的入口,代码如下:

module("luci.controller.njitclient", package.seeall)function index()        entry({
"admin", "network", "njitclient"}, cbi("njitclient"), _("NJIT Client"), 100) end

【二、 用Lua和UCI接口开发LuCI配置模块

我们要做的实际上就是希望能将用户名、密码等信息存储在路由器文件中,同时路由器开机时能根据设定的配置自动运行njit-client,同时我们还希望能动态的禁用和启用njit-client等等。所以最方便的方式就是使用CBI Module,上一节我们也添加了这个调用,那么接下来我们就要根据上边写的路径来创建/usr/lib/lua/luci/model/cbi/njitclient.lua文件。

开发LuCI的配置模块有很多种方式,比较基本的可以用SimpleForm,就跟开发普通的Web应用类似,当然最方便的还是使用UCI(Unified Configuration Interface,统一配置接口)的方式,因为使用UCI接口可以使得在LuCI中可以无需考虑配置文件如何存储和读取(这种方式也会自动创建“保存&应用”、“保存”以及“复位”三个按钮),同时在Bash文件中也可以非常方便的存储和读取。

对于使用UCI的方式,我们首先需要创建对应的配置文件(如果配置文件不存在的话,访问配置页面将会报错),格式即为linux配置文件的格式,文件需要存储在/etc/config,比如文件路径为“/etc/config/njitclient”,内容如下:

config login    option username ''    option password ''    option ifname 'eth0'    option domain ''

然后我们要在CBI Module的lua文件中首先需要映射与存储文件的关系,比如:

m = Map("配置文件文件名", "配置页面标题", "配置页面说明")

第一个参数即为配置文件存储的文件名,不包含路径,比如按上述创建的话,应该写为“njitclient”,而第二与第三个参数则是用在来页面上显示的,比如如下所示的图:

接下来需要创建与配置文件中对应的Section,Section分为两种,NamedSection和TypedSection,前者根据配置文件中的Section名,而后者根据配置文件中的Section类型,这里我们使用后者,代码如下。同时我们设定不允许增加或删除Section(“.addremove = false”),以及不显示Section的名称(“.anonymous = true”)。

s = m:section(TypedSection, "login", "")s.addremove = falses.anonymous = true

接下来我们需要创建Section中不同内容的交互(创建Option),常见的比如有Value(文本框)、ListValue(下拉框)、Flag(选择框)等等,详细的可以参考官方的文档:

创建Option的过程非常简单,而且创建后系统会无需考虑读取以及写入配置文件的问题,系统都会自动处理。但是根据上述的要求,我们在应用配置以后可能希望启用、禁用或重新启动njit-client,所以我们还需要在页面最后判断用户是否点击了“应用”按钮,这里与编写asp网页等都是相同的,我们可以通过如下的代码判断是否点击了“应用”按钮:

local apply = luci.http.formvalue("cbi.apply")if apply then    --[[        需要处理的代码    ]]--end

由于剩余的代码都非常简单,所以所以这部分的全部代码见下:

1 require("luci.sys") 2  3 m = Map("njitclient", translate("NJIT Client"), translate("Configure NJIT 802.11x client.")) 4  5 s = m:section(TypedSection, "login", "") 6 s.addremove = false 7 s.anonymous = true 8  9 enable = s:option(Flag, "enable", translate("Enable"))10 name = s:option(Value, "username", translate("Username"))11 pass = s:option(Value, "password", translate("Password"))12 pass.password = true13 domain = s:option(Value, "domain", translate("Domain"))14 15 ifname = s:option(ListValue, "ifname", translate("Interfaces"))16 for k, v in ipairs(luci.sys.net.devices()) do17     if v ~= "lo" then18         ifname:value(v)19     end20 end21 22 local apply = luci.http.formvalue("cbi.apply")23 if apply then24     io.popen("/etc/init.d/njitclient restart")25 end26 27 return m

其中Luci全部类库的函数定义和使用说明可以参考如下地址:

  1. <ul id="savemenu" class="dropdowns">  
  2. <li><% if ucic > 0 then %><a class="warning" href="<%=controller%>/<%=category%>/uci/changes/"><%:unsavedchanges%>: <%=ucic%></a><%  
  3. submenu("/" .. category .. "/uci/", tree.nodes[category].nodes["uci"])  
  4. else -%>  
  5. <a href="#" mce_href="#"><%:changes%>: 0</a><% end -%>  
  6. </li>  
  7. </ul><% end %>  
  8. <div class="clear"></div>  
  9. </div>  
  10. <div id="maincontent">  

下面以 SimpleForm为例,讲解一下。

具体文件 */luci/model/cbi/passwd.lua

f = SimpleForm("password", translate("a_s_changepw"), translate("a_s_changepw1"))  --调用SimpleForm 页面  当然还是 I18N 从中捣乱,看上去没那么直观,不理他 

pw1=f:field(Value,"pw1",translate("password")) --  SimpleForm 里面加一个 field   至于 SimpleForm  和 fiemd 是什么,一会去看 SimpleForm 页面去 
pw1.rmempty=false -- 把 SimpleForm的 rmempty 为不显示  后面就不做注释了 应该看得懂了 
pw2 = f:field(Value, "pw2", translate("confirmation")) 
pw2.rmempty = false 
function pw2.validate(self, value, section) 
     return pw1:formvalue(section) == value and value 
end 
function f.handle(self, state, data) 
     if   state == FORM_VALID   then     --这个就是业务处理了  你懂得  呵呵 
          local stat = luci.sys.user.setpasswd("admin", data.pw1) == 0  -- root --> admin     
          if stat then 
               f.message = translate("a_s_changepw_changed") 
          else 
               f.errmessage = translate("unknownerror") 
          end 
          
          data.pw1 = nil 
          data.pw2 = nil 
     end 
     return true 
end 
return f

说明:( simpleForm  位于 view/cbi   下面,可以研究一下,各个元素是如何定义的 )

现在在给一个小例子:

.*/luci/model /cbi/admin_system/version_manage.lua 为例,介绍一下 luci 中 web 页面 lua 代码 

  6 local h = loadfile("/usr/local/luci/help.lua") 
  7 if h then 
  8     h() 
  9 end 
10 local help_txt = help_info and  help_info.version 
加载帮助帮助文件help.lua, 关于 loadfile() 的用法可以查看 lua 的手册 ( 我还没完全弄明白,先用了 ) 
help_txt 是一个全局变量 
12 appadmin_path = "/usr/local/appadmin/bin/" 
定义一个全局变量,其实跟功能跟宏一样,定义appadmin 的绝对路径 
14 versionlist = {} 
15 
16 function getline (s) 
......... 
32 end 
33 
34 function get_versionlist() 
......... 
68 end 
69 
70 versionlist = get_versionlist() 
定义一个全局变量和两个函数,并初始化此变量 
接下来就是和最终展现的Web 页面直接相关的代码了,大部分都是对 luci 封装好的一些 html 控件(代码)的使用和扩展。 luci  封装好的 html 控件 
类可以在以下文件查看:./host/usr/lib/lua/luci/cbi.lua 
71 m = SimpleForm("version", translate("版本管理 ")) 
72 m.submit = false 
73 m.reset = false 
74 m.help = help_txt and true or false 
75 m.helptxt = help_txt or "" 
使用了一个SimpleForm 的控件, SimpleForm 实际上对应一个 html 表单,是整个页面最大的 " 容器 " ,本页面内的绝大部分控件都处于 SimpleForm 内 
,是它的子控件 。我知道的可以做> 页面最大 " 容器 " 的控件还有一个 Map, 但它需要 ./host/etc/config/ 目录下的一个配置文件,我没有使用。 
submit reset是 luci 默认就封装好的一个属性,分别控制 html 表单的 " 提交 "" 重置 " 按钮 ;help helptxt 是我扩充的表单属性,分别控制 web 页面的 
"帮助 " 功能和帮助内容。关于控件属 
性的意义、实现和扩充可以按以下步骤进行: 
    在文件./host/usr/lib/lua/luci/cbi.lua 中查找控件名 SimpleForm,  然后可以找到以下行 664     self.template = "cbi/simpleform" 这 
表明SimpleForm 的 html 模版文件为 ./host/usr/lib/lua/luci/view/cbi /simpleform.htm ,通过研究 simpleform.htm 文件内容可以知道各属性的 
功能以及模版中使用lua 代码的方法,然后可以按类似的方法添加自定义的 
属性。 
77 s = m:section(Table, versionlist) 
新建了一个section,section 内定义了一个表格类, versionlist 是与其相关的变量( lua 的所有变量都可归类于 table 类型 ) 
Table 关联的 table 变量应该是这种结构的: 
t = {
 
    row1 = {column1 = "xxx", column2 = "xxx", .... }, 
    row2 = {column1 = "xxx", column2 = "xxx", .... }, 
    row3 = {column1 = "xxx", column2 = "xxx", .... }, 
    row4 = {column1 = "xxx", column2 = "xxx", .... }, 
} 
然后定义Table 的列控件 
79 enable = s:option(DummyValue, "_enabled", translate("软件状态 ")) 
83 appid  = s:option(DummyValue, "_appid", translate("软件版本 ")) 
84 appname = s:option(DummyValue, "_appname", translate("软件名称 ")) 
DummyValue是只读的文本框,只输出不输入。 Value 是单行文本框,可输出也可输入。 Flag 是一个 checkbox,值为 "1" 时被选中,为 "0" 时未选中。 
ListValue是列表框 ... 具体的用法可 
以看./host/usr/lib/lua/luci /model/cbi/ 下的文件( find ./host/usr/lib/lua/luci/model/cbi/ -name "*.lua" |xargs grep 
"ListValue") 
对于table 内普通的字符串类的值,只需要把列控件的 id (括号内第二个值,如 "_appid" )定义为 table 内对应的变量名(比如 column1 ) 
对于非变通字符串类的值,或者为字符串但需要进行一定的处理然后再显示的值,可以按以下方法显示:定义该控件的cfgvalue 函数属性 
127     newinfo = up_s:option(TextValue, "_newifo", translate("新版本信息 ")) 
128     newinfo.readonly = true 
129     newinfo.rows = 11 
130     newinfo.cfgvalue = function(self, section) 
131         local t = string.gsub(info, "Archive:[^/n]*", "") 
132         return t 
133     end 
定义cfgvalue 后, luci 的处理函数会调用此函数为此控件赋值,(传入的 section 参数值为 row1/row2/row3等,当处理到 row 几时值就为 row 几 ) 
对于DummyValue 等只输出不输入的类,还有一种赋值方法: 控件实例名(如 enable).value = xxx 
对于有输入的控件Value 等,  .value 方法赋值在处理输入里会有一些问题,有什么问题以及如何解决可以做实验试试  , 也许是我使用方法不对造 
成的 
对有输入控件的处理有两种方法: 
1 定义控件的 .write 属性 
    这种方法对处理比较独立的输入(与其它控件输入关系不大)比较适用 
88 up_s = m:section(SimpleSection) 
89 up_version = up_s:option(Button, "_up_version", translate("上传新版本 ")) 
90 up_version.onlybutton = true 
91 up_version.align = "right" 
92 up_version.inputstyle = "save" 
93 up_version.write = function(self, section) 
94     luci.http.redirect(luci.dispatcher.build_url("admin", "system", "version_manage", "upload"))
95 end 
ps:只有当 Value 的 rmempty == false 时, Value 输入为空也会触发 write 函数  需要对 rmemtpy 显示赋值为false ( xx.rmempty = false) 

4: view 下面的 html 简介

这个是最好理解的  例:passwd.htm

<%+header%>

<h2><a id="content" name="content"><%:system%></a></h2>

<h3><%:reboot%></h3>

<p><%:a_s_reboot1%></p>

<%-

local c = require("luci.model.uci").cursor():changes()

if c and next(c) then

-%>

       <p class="warning"><%:a_s_reboot_u%></p>

<%-

end

if not reboot then

-%>

<p><a href="<%=controller%>/admin/system/reboot?reboot=1"><%:a_s_reboot_do%></a></p>

<%- else -%>

<p><%:a_s_reboot_running%></p>

<script type="text/javascript">setTimeout("location='<%=controller%>/admin'", 60000)</script>

<%- end -%>

<%+footer%>

<%+header%> <%+footer%>  加载公用的头部和尾部

<% lua code%>

<%:i18n%>

<%lua code%>

<%=lua 变量 %>

【三、在Bash文件中调用UCI接口】

上边我们已经完成了LuCI配置界面的开发,在配置界面中我们已经能读取并保存配置文件了。接下来我们要编写/etc/init.d/njitclient脚本,使程序最终能运行起来。关于UCI接口在脚本文件中的官方说明可以参考:

要使用UCI调用脚本,首先第一步需要读取配置文件,命令为“config_load 配置文件名”,比如我们可以这样读入刚才的配置文件:

config_load njitclient

接下来要遍历配置文件中的Section,可以使用“config_foreach 遍历函数名 Section类型”,例如我们可以这样:

config_foreach run_njit login

然后我们去编写名为“run_njit”的函数,在这个函数中,我们可以使用“config_get 变量名 Section名 Section参数名”获取变量的值,或者使用“config_get_bool 变量名 Section名 Section参数名”获取布尔型的值。所以全部的代码见下:

1 #!/bin/sh /etc/rc.common 2 START=50 3  4 run_njit() 5 { 6     local enable 7     config_get_bool enable $1 enable 8      9     if [ $enable ]; then10         local username11         local password12         local domain13         local ifname14         15         config_get username $1 username16         config_get password $1 password17         config_get domain $1 domain18         config_get ifname $1 ifname19         20         if [ "$domain" != "" ]; then21             njit-client $username@$domain $password $ifname &22         else23             njit-client $username $password $ifname &24         fi25         26         echo "NJIT Client has started."27     fi28 }29 30 start()31 {32     config_load njitclient33     config_foreach run_njit login34 }35 36 stop()37 {38     killall njit-client39     killall udhcpc40     41     echo "NJIT Client has stoped."42 }

【四、 编译开发的程序

如果按上述内容创建好上述4个文件,那么配置页面和程序就能在OpenWrt上运行起来了。但是如果要想把自己写的程序打包,还需要创建OpenWrt的Makefile来使用OpenWrt的SDK进行编译。

关于LuCI上配置Makefile的官方说明可以见这个地址:

无非就是定义包的名称(PKG_NAME)、版本和生成次数(PKG_VERSION、PKG_RELEASE)、在menuconfig中的分类说明等(define Package/luci-app-njitclient)以及安装时进行的操作(define Package/luci-app-njitclient/install)等等。其中安装的文件分为三种,分别是配置文件、可执行文件以及其他数据文件,其中配置可执行文件时,会自动加入执行权限的,所以不需要额外进行处理。Makefile全部的代码见下:

1 include $(TOPDIR)/rules.mk 2  3 PKG_NAME:=luci-app-njitclient 4 PKG_VERSION=1.0 5 PKG_RELEASE:=1 6  7 PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME) 8  9 include $(INCLUDE_DIR)/package.mk10 11 define Package/luci-app-njitclient12     SECTION:=luci13     CATEGORY:=LuCI14     SUBMENU:=3. Applications15     TITLE:=NJIT 802.1X Client for LuCI16     PKGARCH:=all17 endef18 19 define Package/luci-app-njitclient/description20     This package contains LuCI configuration pages for njit8021xclient.21 endef22 23 define Build/Prepare24 endef25 26 define Build/Configure27 endef28 29 define Build/Compile30 endef31 32 define Package/luci-app-njitclient/install33     $(INSTALL_DIR) $(1)/etc/config34     $(INSTALL_DIR) $(1)/etc/init.d35     $(INSTALL_DIR) $(1)/usr/lib/lua/luci/model/cbi36     $(INSTALL_DIR) $(1)/usr/lib/lua/luci/controller37     38     $(INSTALL_CONF) ./files/root/etc/config/njitclient $(1)/etc/config/njitclient39     $(INSTALL_BIN) ./files/root/etc/init.d/njitclient $(1)/etc/init.d/njitclient40     $(INSTALL_DATA) ./files/root/usr/lib/lua/luci/model/cbi/njitclient.lua $(1)/usr/lib/lua/luci/model/cbi/njitclient.lua41     $(INSTALL_DATA) ./files/root/usr/lib/lua/luci/controller/njitclient.lua $(1)/usr/lib/lua/luci/controller/njitclient.lua42 endef43 44 $(eval $(call BuildPackage,luci-app-njitclient))

接下来在编译目录下的package目录下创建一个文件夹,如njitclient,然后将所有的文件按目录复制到该目录下即可。之后配置好OpenWrt的交叉编译环境后就可以使用OpenWrt SDK进行编译了,由于这类文章较多,故不再赘述,可以参考相关链接3及之后的文章。

转载地址:https://blog.csdn.net/yyyyyyyyyywwwwwwwwww/article/details/43561839 如侵犯您的版权,请留言回复原文章的地址,我们会给您删除此文章,给您带来不便请您谅解!

上一篇:/sys/class/gpio
下一篇:OpenWRT 增加内核模块及应用方法

发表评论

最新留言

留言是一种美德,欢迎回访!
[***.207.175.100]2024年04月12日 08时47分42秒