5 Utilities for interaction with the Linux system
11 Copyright 2008 Steven Barth <steven@midlink.org>
13 Licensed under the Apache License, Version 2.0 (the "License");
14 you may not use this file except in compliance with the License.
15 You may obtain a copy of the License at
17 http://www.apache.org/licenses/LICENSE-2.0
19 Unless required by applicable law or agreed to in writing, software
20 distributed under the License is distributed on an "AS IS" BASIS,
21 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
22 See the License for the specific language governing permissions and
23 limitations under the License.
28 local io = require "io"
29 local os = require "os"
30 local table = require "table"
31 local nixio = require "nixio"
32 local fs = require "nixio.fs"
33 local uci = require "luci.model.uci"
36 luci.util = require "luci.util"
37 luci.ip = require "luci.ip"
39 local tonumber, ipairs, pairs, pcall, type, next, setmetatable, require, select =
40 tonumber, ipairs, pairs, pcall, type, next, setmetatable, require, select
43 --- LuCI Linux and POSIX system utilities.
46 --- Execute a given shell command and return the error code
49 -- @param ... Command to call
50 -- @return Error code of the command
52 return os.execute(...) / 256
55 --- Execute a given shell command and capture its standard output
58 -- @param command Command to call
59 -- @return String containg the return the output of the command
62 --- Retrieve information about currently mounted file systems.
63 -- @return Table containing mount information
66 local k = {"fs", "blocks", "used", "available", "percent", "mountpoint"}
67 local ps = luci.util.execi("df")
79 for value in line:gmatch("[^%s]+") do
86 -- this is a rather ugly workaround to cope with wrapped lines in
89 -- /dev/scsi/host0/bus0/target0/lun0/part3
90 -- 114382024 93566472 15005244 86% /mnt/usb
96 for value in line:gmatch("[^%s]+") do
102 table.insert(data, row)
109 --- Retrieve environment variables. If no variable is given then a table
110 -- containing the whole environment is returned otherwise this function returns
111 -- the corresponding string value for the given name or nil if no such variable
115 -- @param var Name of the environment variable to retrieve (optional)
116 -- @return String containg the value of the specified variable
117 -- @return Table containing all variables if no variable name is given
118 getenv = nixio.getenv
120 --- Get or set the current hostname.
121 -- @param String containing a new hostname to set (optional)
122 -- @return String containing the system hostname
123 function hostname(newname)
124 if type(newname) == "string" and #newname > 0 then
125 fs.writefile( "/proc/sys/kernel/hostname", newname )
128 return nixio.uname().nodename
132 --- Returns the contents of a documented referred by an URL.
133 -- @param url The URL to retrieve
134 -- @param stream Return a stream instead of a buffer
135 -- @param target Directly write to target file name
136 -- @return String containing the contents of given the URL
137 function httpget(url, stream, target)
139 local source = stream and io.popen or luci.util.exec
140 return source("wget -qO- '"..url:gsub("'", "").."'")
142 return os.execute("wget -qO '%s' '%s'" %
143 {target:gsub("'", ""), url:gsub("'", "")})
147 --- Initiate a system reboot.
148 -- @return Return value of os.execute()
150 return os.execute("reboot >/dev/null 2>&1")
153 --- Retrieves the output of the "logread" command.
154 -- @return String containing the current log buffer
156 return luci.util.exec("logread")
159 --- Retrieves the output of the "dmesg" command.
160 -- @return String containing the current log buffer
162 return luci.util.exec("dmesg")
165 --- Generates a random id with specified length.
166 -- @param bytes Number of bytes for the unique id
167 -- @return String containing hex encoded id
168 function uniqueid(bytes)
169 local rand = fs.readfile("/dev/urandom", bytes)
170 return rand and nixio.bin.hexlify(rand)
173 --- Returns the current system uptime stats.
174 -- @return String containing total uptime in seconds
176 return nixio.sysinfo().uptime
180 --- LuCI system utilities / network related functions.
182 -- @name luci.sys.net
185 --- Returns the current arp-table entries as two-dimensional table.
186 -- @return Table of table containing the current arp entries.
187 -- The following fields are defined for arp entry objects:
188 -- { "IP address", "HW address", "HW type", "Flags", "Mask", "Device" }
189 function net.arptable(callback)
190 local arp = (not callback) and {} or nil
192 if fs.access("/proc/net/arp") then
193 for e in io.lines("/proc/net/arp") do
195 for v in e:gmatch("%S+") do
201 ["IP address"] = r[1],
204 ["HW address"] = r[4],
221 local function _nethints(what, callback)
222 local _, k, e, mac, ip, name
223 local cur = uci.cursor()
227 local function _add(i, ...)
228 local k = select(i, ...)
230 if not hosts[k] then hosts[k] = { } end
231 hosts[k][1] = select(1, ...) or hosts[k][1]
232 hosts[k][2] = select(2, ...) or hosts[k][2]
233 hosts[k][3] = select(3, ...) or hosts[k][3]
234 hosts[k][4] = select(4, ...) or hosts[k][4]
238 if fs.access("/proc/net/arp") then
239 for e in io.lines("/proc/net/arp") do
240 ip, mac = e:match("^([%d%.]+)%s+%S+%s+%S+%s+([a-fA-F0-9:]+)%s+")
242 _add(what, mac:upper(), ip, nil, nil)
247 if fs.access("/etc/ethers") then
248 for e in io.lines("/etc/ethers") do
249 mac, ip = e:match("^([a-f0-9]%S+) (%S+)")
251 _add(what, mac:upper(), ip, nil, nil)
256 if fs.access("/var/dhcp.leases") then
257 for e in io.lines("/var/dhcp.leases") do
258 mac, ip, name = e:match("^%d+ (%S+) (%S+) (%S+)")
260 _add(what, mac:upper(), ip, nil, name ~= "*" and name)
265 cur:foreach("dhcp", "host",
267 for mac in luci.util.imatch(s.mac) do
268 _add(what, mac:upper(), s.ip, nil, s.name)
272 for _, e in ipairs(nixio.getifaddrs()) do
273 if e.name ~= "lo" then
274 ifn[e.name] = ifn[e.name] or { }
275 if e.family == "packet" and e.addr and #e.addr == 17 then
276 ifn[e.name][1] = e.addr:upper()
277 elseif e.family == "inet" then
278 ifn[e.name][2] = e.addr
279 elseif e.family == "inet6" then
280 ifn[e.name][3] = e.addr
285 for _, e in pairs(ifn) do
286 if e[what] and (e[2] or e[3]) then
287 _add(what, e[1], e[2], e[3], e[4])
291 for _, e in luci.util.kspairs(hosts) do
292 callback(e[1], e[2], e[3], e[4])
296 --- Returns a two-dimensional table of mac address hints.
297 -- @return Table of table containing known hosts from various sources.
298 -- Each entry contains the values in the following order:
300 function net.mac_hints(callback)
302 _nethints(1, function(mac, v4, v6, name)
303 name = name or nixio.getnameinfo(v4 or v6, nil, 100) or v4
304 if name and name ~= mac then
305 callback(mac, name or nixio.getnameinfo(v4 or v6, nil, 100) or v4)
310 _nethints(1, function(mac, v4, v6, name)
311 name = name or nixio.getnameinfo(v4 or v6, nil, 100) or v4
312 if name and name ~= mac then
313 rv[#rv+1] = { mac, name or nixio.getnameinfo(v4 or v6, nil, 100) or v4 }
320 --- Returns a two-dimensional table of IPv4 address hints.
321 -- @return Table of table containing known hosts from various sources.
322 -- Each entry contains the values in the following order:
324 function net.ipv4_hints(callback)
326 _nethints(2, function(mac, v4, v6, name)
327 name = name or nixio.getnameinfo(v4, nil, 100) or mac
328 if name and name ~= v4 then
334 _nethints(2, function(mac, v4, v6, name)
335 name = name or nixio.getnameinfo(v4, nil, 100) or mac
336 if name and name ~= v4 then
337 rv[#rv+1] = { v4, name }
344 --- Returns a two-dimensional table of IPv6 address hints.
345 -- @return Table of table containing known hosts from various sources.
346 -- Each entry contains the values in the following order:
348 function net.ipv6_hints(callback)
350 _nethints(3, function(mac, v4, v6, name)
351 name = name or nixio.getnameinfo(v6, nil, 100) or mac
352 if name and name ~= v6 then
358 _nethints(3, function(mac, v4, v6, name)
359 name = name or nixio.getnameinfo(v6, nil, 100) or mac
360 if name and name ~= v6 then
361 rv[#rv+1] = { v6, name }
368 --- Returns conntrack information
369 -- @return Table with the currently tracked IP connections
370 function net.conntrack(callback)
372 if fs.access("/proc/net/nf_conntrack", "r") then
373 for line in io.lines("/proc/net/nf_conntrack") do
374 line = line:match "^(.-( [^ =]+=).-)%2"
375 local entry, flags = _parse_mixed_record(line, " +")
376 if flags[6] ~= "TIME_WAIT" then
377 entry.layer3 = flags[1]
378 entry.layer4 = flags[3]
386 connt[#connt+1] = entry
390 elseif fs.access("/proc/net/ip_conntrack", "r") then
391 for line in io.lines("/proc/net/ip_conntrack") do
392 line = line:match "^(.-( [^ =]+=).-)%2"
393 local entry, flags = _parse_mixed_record(line, " +")
394 if flags[4] ~= "TIME_WAIT" then
395 entry.layer3 = "ipv4"
396 entry.layer4 = flags[1]
404 connt[#connt+1] = entry
414 --- Determine the current IPv4 default route. If multiple default routes exist,
415 -- return the one with the lowest metric.
416 -- @return Table with the properties of the current default route.
417 -- The following fields are defined:
418 -- { "dest", "gateway", "metric", "refcount", "usecount", "irtt",
419 -- "flags", "device" }
420 function net.defaultroute()
423 net.routes(function(rt)
424 if rt.dest:prefix() == 0 and (not route or route.metric > rt.metric) then
432 --- Determine the current IPv6 default route. If multiple default routes exist,
433 -- return the one with the lowest metric.
434 -- @return Table with the properties of the current default route.
435 -- The following fields are defined:
436 -- { "source", "dest", "nexthop", "metric", "refcount", "usecount",
437 -- "flags", "device" }
438 function net.defaultroute6()
441 net.routes6(function(rt)
442 if rt.dest:prefix() == 0 and rt.device ~= "lo" and
443 (not route or route.metric > rt.metric)
450 local global_unicast = luci.ip.IPv6("2000::/3")
451 net.routes6(function(rt)
452 if rt.dest:equal(global_unicast) and
453 (not route or route.metric > rt.metric)
463 --- Determine the names of available network interfaces.
464 -- @return Table containing all current interface names
465 function net.devices()
467 for k, v in ipairs(nixio.getifaddrs()) do
468 if v.family == "packet" then
469 devs[#devs+1] = v.name
476 --- Return information about available network interfaces.
477 -- @return Table containing all current interface names and their information
478 function net.deviceinfo()
480 for k, v in ipairs(nixio.getifaddrs()) do
481 if v.family == "packet" then
506 -- Determine the MAC address belonging to the given IP address.
507 -- @param ip IPv4 address
508 -- @return String containing the MAC address or nil if it cannot be found
509 function net.ip4mac(ip)
511 net.arptable(function(e)
512 if e["IP address"] == ip then
513 mac = e["HW address"]
519 --- Returns the current kernel routing table entries.
520 -- @return Table of tables with properties of the corresponding routes.
521 -- The following fields are defined for route entry tables:
522 -- { "dest", "gateway", "metric", "refcount", "usecount", "irtt",
523 -- "flags", "device" }
524 function net.routes(callback)
527 for line in io.lines("/proc/net/route") do
529 local dev, dst_ip, gateway, flags, refcnt, usecnt, metric,
530 dst_mask, mtu, win, irtt = line:match(
531 "([^%s]+)\t([A-F0-9]+)\t([A-F0-9]+)\t([A-F0-9]+)\t" ..
532 "(%d+)\t(%d+)\t(%d+)\t([A-F0-9]+)\t(%d+)\t(%d+)\t(%d+)"
536 gateway = luci.ip.Hex( gateway, 32, luci.ip.FAMILY_INET4 )
537 dst_mask = luci.ip.Hex( dst_mask, 32, luci.ip.FAMILY_INET4 )
538 dst_ip = luci.ip.Hex(
539 dst_ip, dst_mask:prefix(dst_mask), luci.ip.FAMILY_INET4
545 metric = tonumber(metric),
546 refcount = tonumber(refcnt),
547 usecount = tonumber(usecnt),
549 window = tonumber(window),
550 irtt = tonumber(irtt),
551 flags = tonumber(flags, 16),
558 routes[#routes+1] = rt
566 --- Returns the current ipv6 kernel routing table entries.
567 -- @return Table of tables with properties of the corresponding routes.
568 -- The following fields are defined for route entry tables:
569 -- { "source", "dest", "nexthop", "metric", "refcount", "usecount",
570 -- "flags", "device" }
571 function net.routes6(callback)
572 if fs.access("/proc/net/ipv6_route", "r") then
575 for line in io.lines("/proc/net/ipv6_route") do
577 local dst_ip, dst_prefix, src_ip, src_prefix, nexthop,
578 metric, refcnt, usecnt, flags, dev = line:match(
579 "([a-f0-9]+) ([a-f0-9]+) " ..
580 "([a-f0-9]+) ([a-f0-9]+) " ..
581 "([a-f0-9]+) ([a-f0-9]+) " ..
582 "([a-f0-9]+) ([a-f0-9]+) " ..
583 "([a-f0-9]+) +([^%s]+)"
586 if dst_ip and dst_prefix and
587 src_ip and src_prefix and
588 nexthop and metric and
589 refcnt and usecnt and
592 src_ip = luci.ip.Hex(
593 src_ip, tonumber(src_prefix, 16), luci.ip.FAMILY_INET6, false
596 dst_ip = luci.ip.Hex(
597 dst_ip, tonumber(dst_prefix, 16), luci.ip.FAMILY_INET6, false
600 nexthop = luci.ip.Hex( nexthop, 128, luci.ip.FAMILY_INET6, false )
606 metric = tonumber(metric, 16),
607 refcount = tonumber(refcnt, 16),
608 usecount = tonumber(usecnt, 16),
609 flags = tonumber(flags, 16),
612 -- lua number is too small for storing the metric
613 -- add a metric_raw field with the original content
620 routes[#routes+1] = rt
629 --- Tests whether the given host responds to ping probes.
630 -- @param host String containing a hostname or IPv4 address
631 -- @return Number containing 0 on success and >= 1 on error
632 function net.pingtest(host)
633 return os.execute("ping -c1 '"..host:gsub("'", '').."' >/dev/null 2>&1")
637 --- LuCI system utilities / process related functions.
639 -- @name luci.sys.process
642 --- Get the current process id.
644 -- @name process.info
645 -- @return Number containing the current pid
646 function process.info(key)
647 local s = {uid = nixio.getuid(), gid = nixio.getgid()}
648 return not key and s or s[key]
651 --- Retrieve information about currently running processes.
652 -- @return Table containing process information
653 function process.list()
656 local ps = luci.util.execi("/bin/busybox top -bn1")
663 local pid, ppid, user, stat, vsz, mem, cpu, cmd = line:match(
664 "^ *(%d+) +(%d+) +(%S.-%S) +([RSDZTW][W ][<N ]) +(%d+) +(%d+%%) +(%d+%%) +(.+)"
667 local idx = tonumber(pid)
685 --- Set the gid of a process identified by given pid.
686 -- @param gid Number containing the Unix group id
687 -- @return Boolean indicating successful operation
688 -- @return String containing the error message if failed
689 -- @return Number containing the error code if failed
690 function process.setgroup(gid)
691 return nixio.setgid(gid)
694 --- Set the uid of a process identified by given pid.
695 -- @param uid Number containing the Unix user id
696 -- @return Boolean indicating successful operation
697 -- @return String containing the error message if failed
698 -- @return Number containing the error code if failed
699 function process.setuser(uid)
700 return nixio.setuid(uid)
703 --- Send a signal to a process identified by given pid.
705 -- @name process.signal
706 -- @param pid Number containing the process id
707 -- @param sig Signal to send (default: 15 [SIGTERM])
708 -- @return Boolean indicating successful operation
709 -- @return Number containing the error code if failed
710 process.signal = nixio.kill
713 --- LuCI system utilities / user related functions.
715 -- @name luci.sys.user
718 --- Retrieve user informations for given uid.
721 -- @param uid Number containing the Unix user id
722 -- @return Table containing the following fields:
723 -- { "uid", "gid", "name", "passwd", "dir", "shell", "gecos" }
724 user.getuser = nixio.getpw
726 --- Retrieve the current user password hash.
727 -- @param username String containing the username to retrieve the password for
728 -- @return String containing the hash or nil if no password is set.
729 -- @return Password database entry
730 function user.getpasswd(username)
731 local pwe = nixio.getsp and nixio.getsp(username) or nixio.getpw(username)
732 local pwh = pwe and (pwe.pwdp or pwe.passwd)
733 if not pwh or #pwh < 1 or pwh == "!" or pwh == "x" then
740 --- Test whether given string matches the password of a given system user.
741 -- @param username String containing the Unix user name
742 -- @param pass String containing the password to compare
743 -- @return Boolean indicating wheather the passwords are equal
744 function user.checkpasswd(username, pass)
745 local pwh, pwe = user.getpasswd(username)
747 return (pwh == nil or nixio.crypt(pass, pwh) == pwh)
752 --- Change the password of given user.
753 -- @param username String containing the Unix user name
754 -- @param password String containing the password to compare
755 -- @return Number containing 0 on success and >= 1 on error
756 function user.setpasswd(username, password)
758 password = password:gsub("'", [['"'"']])
762 username = username:gsub("'", [['"'"']])
766 "(echo '" .. password .. "'; sleep 1; echo '" .. password .. "') | " ..
767 "passwd '" .. username .. "' >/dev/null 2>&1"
772 --- LuCI system utilities / wifi related functions.
774 -- @name luci.sys.wifi
777 --- Get wireless information for given interface.
778 -- @param ifname String containing the interface name
779 -- @return A wrapped iwinfo object instance
780 function wifi.getiwinfo(ifname)
781 local stat, iwinfo = pcall(require, "iwinfo")
785 local u = uci.cursor_state()
786 local d, n = ifname:match("^(%w+)%.network(%d+)")
790 u:foreach("wireless", "wifi-iface",
792 if s.device == d then
795 ifname = s.ifname or s.device
800 elseif u:get("wireless", ifname) == "wifi-device" then
801 u:foreach("wireless", "wifi-iface",
803 if s.device == ifname and s.ifname then
810 local t = stat and iwinfo.type(ifname)
811 local x = t and iwinfo[t] or { }
812 return setmetatable({}, {
813 __index = function(t, k)
814 if k == "ifname" then
825 --- LuCI system utilities / init related functions.
827 -- @name luci.sys.init
829 init.dir = "/etc/init.d/"
831 --- Get the names of all installed init scripts
832 -- @return Table containing the names of all inistalled init scripts
833 function init.names()
835 for name in fs.glob(init.dir.."*") do
836 names[#names+1] = fs.basename(name)
841 --- Get the index of he given init script
842 -- @param name Name of the init script
843 -- @return Numeric index value
844 function init.index(name)
845 if fs.access(init.dir..name) then
846 return call("env -i sh -c 'source %s%s enabled; exit ${START:-255}' >/dev/null"
851 local function init_action(action, name)
852 if fs.access(init.dir..name) then
853 return call("env -i %s%s %s >/dev/null" %{ init.dir, name, action })
857 --- Test whether the given init script is enabled
858 -- @param name Name of the init script
859 -- @return Boolean indicating whether init is enabled
860 function init.enabled(name)
861 return (init_action("enabled", name) == 0)
864 --- Enable the given init script
865 -- @param name Name of the init script
866 -- @return Boolean indicating success
867 function init.enable(name)
868 return (init_action("enable", name) == 1)
871 --- Disable the given init script
872 -- @param name Name of the init script
873 -- @return Boolean indicating success
874 function init.disable(name)
875 return (init_action("disable", name) == 0)
878 --- Start the given init script
879 -- @param name Name of the init script
880 -- @return Boolean indicating success
881 function init.start(name)
882 return (init_action("start", name) == 0)
885 --- Stop the given init script
886 -- @param name Name of the init script
887 -- @return Boolean indicating success
888 function init.stop(name)
889 return (init_action("stop", name) == 0)
893 -- Internal functions
895 function _parse_mixed_record(cnt, delimiter)
896 delimiter = delimiter or " "
900 for i, l in pairs(luci.util.split(luci.util.trim(cnt), "\n")) do
901 for j, f in pairs(luci.util.split(luci.util.trim(l), delimiter, nil, true)) do
902 local k, x, v = f:match('([^%s][^:=]*) *([:=]*) *"*([^\n"]*)"*')
906 table.insert(flags, k)