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 --- Returns the system load average values.
148 -- @return String containing the average load value 1 minute ago
149 -- @return String containing the average load value 5 minutes ago
150 -- @return String containing the average load value 15 minutes ago
152 local info = nixio.sysinfo()
153 return info.loads[1], info.loads[2], info.loads[3]
156 --- Initiate a system reboot.
157 -- @return Return value of os.execute()
159 return os.execute("reboot >/dev/null 2>&1")
162 --- Returns the system type, cpu name and installed physical memory.
163 -- @return String containing the system or platform identifier
164 -- @return String containing hardware model information
165 -- @return String containing the total memory amount in kB
166 -- @return String containing the memory used for caching in kB
167 -- @return String containing the memory used for buffering in kB
168 -- @return String containing the free memory amount in kB
169 -- @return String containing the cpu bogomips (number)
171 local cpuinfo = fs.readfile("/proc/cpuinfo")
172 local meminfo = fs.readfile("/proc/meminfo")
174 local memtotal = tonumber(meminfo:match("MemTotal:%s*(%d+)"))
175 local memcached = tonumber(meminfo:match("\nCached:%s*(%d+)"))
176 local memfree = tonumber(meminfo:match("MemFree:%s*(%d+)"))
177 local membuffers = tonumber(meminfo:match("Buffers:%s*(%d+)"))
178 local bogomips = tonumber(cpuinfo:match("[Bb]ogo[Mm][Ii][Pp][Ss].-: ([^\n]+)")) or 0
181 cpuinfo:match("system type\t+: ([^\n]+)") or
182 cpuinfo:match("Processor\t+: ([^\n]+)") or
183 cpuinfo:match("model name\t+: ([^\n]+)")
186 luci.util.pcdata(fs.readfile("/tmp/sysinfo/model")) or
187 cpuinfo:match("machine\t+: ([^\n]+)") or
188 cpuinfo:match("Hardware\t+: ([^\n]+)") or
189 luci.util.pcdata(fs.readfile("/proc/diag/model")) or
190 nixio.uname().machine or
193 return system, model, memtotal, memcached, membuffers, memfree, bogomips
196 --- Retrieves the output of the "logread" command.
197 -- @return String containing the current log buffer
199 return luci.util.exec("logread")
202 --- Retrieves the output of the "dmesg" command.
203 -- @return String containing the current log buffer
205 return luci.util.exec("dmesg")
208 --- Generates a random id with specified length.
209 -- @param bytes Number of bytes for the unique id
210 -- @return String containing hex encoded id
211 function uniqueid(bytes)
212 local rand = fs.readfile("/dev/urandom", bytes)
213 return rand and nixio.bin.hexlify(rand)
216 --- Returns the current system uptime stats.
217 -- @return String containing total uptime in seconds
219 return nixio.sysinfo().uptime
223 --- LuCI system utilities / network related functions.
225 -- @name luci.sys.net
228 --- Returns the current arp-table entries as two-dimensional table.
229 -- @return Table of table containing the current arp entries.
230 -- The following fields are defined for arp entry objects:
231 -- { "IP address", "HW address", "HW type", "Flags", "Mask", "Device" }
232 function net.arptable(callback)
234 if fs.access("/proc/net/arp") then
235 for e in io.lines("/proc/net/arp") do
237 for v in e:gmatch("%S+") do
243 ["IP address"] = r[1],
246 ["HW address"] = r[4],
263 local function _nethints(what, callback)
264 local _, k, e, mac, ip, name
265 local cur = uci.cursor()
269 local function _add(i, ...)
270 local k = select(i, ...)
272 if not hosts[k] then hosts[k] = { } end
273 hosts[k][1] = select(1, ...) or hosts[k][1]
274 hosts[k][2] = select(2, ...) or hosts[k][2]
275 hosts[k][3] = select(3, ...) or hosts[k][3]
276 hosts[k][4] = select(4, ...) or hosts[k][4]
280 if fs.access("/proc/net/arp") then
281 for e in io.lines("/proc/net/arp") do
282 ip, mac = e:match("^([%d%.]+)%s+%S+%s+%S+%s+([a-fA-F0-9:]+)%s+")
284 _add(what, mac:upper(), ip, nil, nil)
289 if fs.access("/etc/ethers") then
290 for e in io.lines("/etc/ethers") do
291 mac, ip = e:match("^([a-f0-9]%S+) (%S+)")
293 _add(what, mac:upper(), ip, nil, nil)
298 if fs.access("/var/dhcp.leases") then
299 for e in io.lines("/var/dhcp.leases") do
300 mac, ip, name = e:match("^%d+ (%S+) (%S+) (%S+)")
302 _add(what, mac:upper(), ip, nil, name ~= "*" and name)
307 cur:foreach("dhcp", "host",
309 for mac in luci.util.imatch(s.mac) do
310 _add(what, mac:upper(), s.ip, nil, s.name)
314 for _, e in ipairs(nixio.getifaddrs()) do
315 if e.name ~= "lo" then
316 ifn[e.name] = ifn[e.name] or { }
317 if e.family == "packet" and e.addr and #e.addr == 17 then
318 ifn[e.name][1] = e.addr:upper()
319 elseif e.family == "inet" then
320 ifn[e.name][2] = e.addr
321 elseif e.family == "inet6" then
322 ifn[e.name][3] = e.addr
327 for _, e in pairs(ifn) do
328 if e[what] and (e[2] or e[3]) then
329 _add(what, e[1], e[2], e[3], e[4])
333 for _, e in luci.util.kspairs(hosts) do
334 callback(e[1], e[2], e[3], e[4])
338 --- Returns a two-dimensional table of mac address hints.
339 -- @return Table of table containing known hosts from various sources.
340 -- Each entry contains the values in the following order:
342 function net.mac_hints(callback)
344 _nethints(1, function(mac, v4, v6, name)
345 name = name or nixio.getnameinfo(v4 or v6) or v4
346 if name and name ~= mac then
347 callback(mac, name or nixio.getnameinfo(v4 or v6) or v4)
352 _nethints(1, function(mac, v4, v6, name)
353 name = name or nixio.getnameinfo(v4 or v6) or v4
354 if name and name ~= mac then
355 rv[#rv+1] = { mac, name or nixio.getnameinfo(v4 or v6) or v4 }
362 --- Returns a two-dimensional table of IPv4 address hints.
363 -- @return Table of table containing known hosts from various sources.
364 -- Each entry contains the values in the following order:
366 function net.ipv4_hints(callback)
368 _nethints(2, function(mac, v4, v6, name)
369 name = name or nixio.getnameinfo(v4) or mac
370 if name and name ~= v4 then
376 _nethints(2, function(mac, v4, v6, name)
377 name = name or nixio.getnameinfo(v4) or mac
378 if name and name ~= v4 then
379 rv[#rv+1] = { v4, name }
386 --- Returns a two-dimensional table of IPv6 address hints.
387 -- @return Table of table containing known hosts from various sources.
388 -- Each entry contains the values in the following order:
390 function net.ipv6_hints(callback)
392 _nethints(3, function(mac, v4, v6, name)
393 name = name or nixio.getnameinfo(v6) or mac
394 if name and name ~= v6 then
400 _nethints(3, function(mac, v4, v6, name)
401 name = name or nixio.getnameinfo(v6) or mac
402 if name and name ~= v6 then
403 rv[#rv+1] = { v6, name }
410 --- Returns conntrack information
411 -- @return Table with the currently tracked IP connections
412 function net.conntrack(callback)
414 if fs.access("/proc/net/nf_conntrack", "r") then
415 for line in io.lines("/proc/net/nf_conntrack") do
416 line = line:match "^(.-( [^ =]+=).-)%2"
417 local entry, flags = _parse_mixed_record(line, " +")
418 if flags[6] ~= "TIME_WAIT" then
419 entry.layer3 = flags[1]
420 entry.layer4 = flags[3]
428 connt[#connt+1] = entry
432 elseif fs.access("/proc/net/ip_conntrack", "r") then
433 for line in io.lines("/proc/net/ip_conntrack") do
434 line = line:match "^(.-( [^ =]+=).-)%2"
435 local entry, flags = _parse_mixed_record(line, " +")
436 if flags[4] ~= "TIME_WAIT" then
437 entry.layer3 = "ipv4"
438 entry.layer4 = flags[1]
446 connt[#connt+1] = entry
456 --- Determine the current IPv4 default route. If multiple default routes exist,
457 -- return the one with the lowest metric.
458 -- @return Table with the properties of the current default route.
459 -- The following fields are defined:
460 -- { "dest", "gateway", "metric", "refcount", "usecount", "irtt",
461 -- "flags", "device" }
462 function net.defaultroute()
465 net.routes(function(rt)
466 if rt.dest:prefix() == 0 and (not route or route.metric > rt.metric) then
474 --- Determine the current IPv6 default route. If multiple default routes exist,
475 -- return the one with the lowest metric.
476 -- @return Table with the properties of the current default route.
477 -- The following fields are defined:
478 -- { "source", "dest", "nexthop", "metric", "refcount", "usecount",
479 -- "flags", "device" }
480 function net.defaultroute6()
483 net.routes6(function(rt)
484 if rt.dest:prefix() == 0 and rt.device ~= "lo" and
485 (not route or route.metric > rt.metric)
492 local global_unicast = luci.ip.IPv6("2000::/3")
493 net.routes6(function(rt)
494 if rt.dest:equal(global_unicast) and
495 (not route or route.metric > rt.metric)
505 --- Determine the names of available network interfaces.
506 -- @return Table containing all current interface names
507 function net.devices()
509 for k, v in ipairs(nixio.getifaddrs()) do
510 if v.family == "packet" then
511 devs[#devs+1] = v.name
518 --- Return information about available network interfaces.
519 -- @return Table containing all current interface names and their information
520 function net.deviceinfo()
522 for k, v in ipairs(nixio.getifaddrs()) do
523 if v.family == "packet" then
548 -- Determine the MAC address belonging to the given IP address.
549 -- @param ip IPv4 address
550 -- @return String containing the MAC address or nil if it cannot be found
551 function net.ip4mac(ip)
553 net.arptable(function(e)
554 if e["IP address"] == ip then
555 mac = e["HW address"]
561 --- Returns the current kernel routing table entries.
562 -- @return Table of tables with properties of the corresponding routes.
563 -- The following fields are defined for route entry tables:
564 -- { "dest", "gateway", "metric", "refcount", "usecount", "irtt",
565 -- "flags", "device" }
566 function net.routes(callback)
569 for line in io.lines("/proc/net/route") do
571 local dev, dst_ip, gateway, flags, refcnt, usecnt, metric,
572 dst_mask, mtu, win, irtt = line:match(
573 "([^%s]+)\t([A-F0-9]+)\t([A-F0-9]+)\t([A-F0-9]+)\t" ..
574 "(%d+)\t(%d+)\t(%d+)\t([A-F0-9]+)\t(%d+)\t(%d+)\t(%d+)"
578 gateway = luci.ip.Hex( gateway, 32, luci.ip.FAMILY_INET4 )
579 dst_mask = luci.ip.Hex( dst_mask, 32, luci.ip.FAMILY_INET4 )
580 dst_ip = luci.ip.Hex(
581 dst_ip, dst_mask:prefix(dst_mask), luci.ip.FAMILY_INET4
587 metric = tonumber(metric),
588 refcount = tonumber(refcnt),
589 usecount = tonumber(usecnt),
591 window = tonumber(window),
592 irtt = tonumber(irtt),
593 flags = tonumber(flags, 16),
600 routes[#routes+1] = rt
608 --- Returns the current ipv6 kernel routing table entries.
609 -- @return Table of tables with properties of the corresponding routes.
610 -- The following fields are defined for route entry tables:
611 -- { "source", "dest", "nexthop", "metric", "refcount", "usecount",
612 -- "flags", "device" }
613 function net.routes6(callback)
614 if fs.access("/proc/net/ipv6_route", "r") then
617 for line in io.lines("/proc/net/ipv6_route") do
619 local dst_ip, dst_prefix, src_ip, src_prefix, nexthop,
620 metric, refcnt, usecnt, flags, dev = line:match(
621 "([a-f0-9]+) ([a-f0-9]+) " ..
622 "([a-f0-9]+) ([a-f0-9]+) " ..
623 "([a-f0-9]+) ([a-f0-9]+) " ..
624 "([a-f0-9]+) ([a-f0-9]+) " ..
625 "([a-f0-9]+) +([^%s]+)"
628 if dst_ip and dst_prefix and
629 src_ip and src_prefix and
630 nexthop and metric and
631 refcnt and usecnt and
634 src_ip = luci.ip.Hex(
635 src_ip, tonumber(src_prefix, 16), luci.ip.FAMILY_INET6, false
638 dst_ip = luci.ip.Hex(
639 dst_ip, tonumber(dst_prefix, 16), luci.ip.FAMILY_INET6, false
642 nexthop = luci.ip.Hex( nexthop, 128, luci.ip.FAMILY_INET6, false )
648 metric = tonumber(metric, 16),
649 refcount = tonumber(refcnt, 16),
650 usecount = tonumber(usecnt, 16),
651 flags = tonumber(flags, 16),
654 -- lua number is too small for storing the metric
655 -- add a metric_raw field with the original content
662 routes[#routes+1] = rt
671 --- Tests whether the given host responds to ping probes.
672 -- @param host String containing a hostname or IPv4 address
673 -- @return Number containing 0 on success and >= 1 on error
674 function net.pingtest(host)
675 return os.execute("ping -c1 '"..host:gsub("'", '').."' >/dev/null 2>&1")
679 --- LuCI system utilities / process related functions.
681 -- @name luci.sys.process
684 --- Get the current process id.
686 -- @name process.info
687 -- @return Number containing the current pid
688 function process.info(key)
689 local s = {uid = nixio.getuid(), gid = nixio.getgid()}
690 return not key and s or s[key]
693 --- Retrieve information about currently running processes.
694 -- @return Table containing process information
695 function process.list()
698 local ps = luci.util.execi("top -bn1")
710 k = luci.util.split(luci.util.trim(line), "%s+", nil, true)
711 if k[6] == "%VSZ" then
714 if k[1] == "PID" then
722 line = luci.util.trim(line)
723 for i, value in ipairs(luci.util.split(line, "%s+", #k-1, true)) do
727 local pid = tonumber(row[k[1]])
736 --- Set the gid of a process identified by given pid.
737 -- @param gid Number containing the Unix group id
738 -- @return Boolean indicating successful operation
739 -- @return String containing the error message if failed
740 -- @return Number containing the error code if failed
741 function process.setgroup(gid)
742 return nixio.setgid(gid)
745 --- Set the uid of a process identified by given pid.
746 -- @param uid Number containing the Unix user id
747 -- @return Boolean indicating successful operation
748 -- @return String containing the error message if failed
749 -- @return Number containing the error code if failed
750 function process.setuser(uid)
751 return nixio.setuid(uid)
754 --- Send a signal to a process identified by given pid.
756 -- @name process.signal
757 -- @param pid Number containing the process id
758 -- @param sig Signal to send (default: 15 [SIGTERM])
759 -- @return Boolean indicating successful operation
760 -- @return Number containing the error code if failed
761 process.signal = nixio.kill
764 --- LuCI system utilities / user related functions.
766 -- @name luci.sys.user
769 --- Retrieve user informations for given uid.
772 -- @param uid Number containing the Unix user id
773 -- @return Table containing the following fields:
774 -- { "uid", "gid", "name", "passwd", "dir", "shell", "gecos" }
775 user.getuser = nixio.getpw
777 --- Retrieve the current user password hash.
778 -- @param username String containing the username to retrieve the password for
779 -- @return String containing the hash or nil if no password is set.
780 -- @return Password database entry
781 function user.getpasswd(username)
782 local pwe = nixio.getsp and nixio.getsp(username) or nixio.getpw(username)
783 local pwh = pwe and (pwe.pwdp or pwe.passwd)
784 if not pwh or #pwh < 1 or pwh == "!" or pwh == "x" then
791 --- Test whether given string matches the password of a given system user.
792 -- @param username String containing the Unix user name
793 -- @param pass String containing the password to compare
794 -- @return Boolean indicating wheather the passwords are equal
795 function user.checkpasswd(username, pass)
796 local pwh, pwe = user.getpasswd(username)
798 return (pwh == nil or nixio.crypt(pass, pwh) == pwh)
803 --- Change the password of given user.
804 -- @param username String containing the Unix user name
805 -- @param password String containing the password to compare
806 -- @return Number containing 0 on success and >= 1 on error
807 function user.setpasswd(username, password)
809 password = password:gsub("'", [['"'"']])
813 username = username:gsub("'", [['"'"']])
817 "(echo '" .. password .. "'; sleep 1; echo '" .. password .. "') | " ..
818 "passwd '" .. username .. "' >/dev/null 2>&1"
823 --- LuCI system utilities / wifi related functions.
825 -- @name luci.sys.wifi
828 --- Get wireless information for given interface.
829 -- @param ifname String containing the interface name
830 -- @return A wrapped iwinfo object instance
831 function wifi.getiwinfo(ifname)
832 local stat, iwinfo = pcall(require, "iwinfo")
836 local u = uci.cursor_state()
837 local d, n = ifname:match("^(%w+)%.network(%d+)")
840 u:foreach("wireless", "wifi-iface",
842 if s.device == d then
845 ifname = s.ifname or s.device
850 elseif u:get("wireless", ifname) == "wifi-device" then
851 u:foreach("wireless", "wifi-iface",
853 if s.device == ifname and s.ifname then
860 local t = stat and iwinfo.type(ifname)
861 local x = t and iwinfo[t] or { }
862 return setmetatable({}, {
863 __index = function(t, k)
864 if k == "ifname" then
875 --- LuCI system utilities / init related functions.
877 -- @name luci.sys.init
879 init.dir = "/etc/init.d/"
881 --- Get the names of all installed init scripts
882 -- @return Table containing the names of all inistalled init scripts
883 function init.names()
885 for name in fs.glob(init.dir.."*") do
886 names[#names+1] = fs.basename(name)
891 --- Get the index of he given init script
892 -- @param name Name of the init script
893 -- @return Numeric index value
894 function init.index(name)
895 if fs.access(init.dir..name) then
896 return call("env -i sh -c 'source %s%s enabled; exit ${START:-255}' >/dev/null"
901 local function init_action(action, name)
902 if fs.access(init.dir..name) then
903 return call("env -i %s%s %s >/dev/null" %{ init.dir, name, action })
907 --- Test whether the given init script is enabled
908 -- @param name Name of the init script
909 -- @return Boolean indicating whether init is enabled
910 function init.enabled(name)
911 return (init_action("enabled", name) == 0)
914 --- Enable the given init script
915 -- @param name Name of the init script
916 -- @return Boolean indicating success
917 function init.enable(name)
918 return (init_action("enable", name) == 1)
921 --- Disable the given init script
922 -- @param name Name of the init script
923 -- @return Boolean indicating success
924 function init.disable(name)
925 return (init_action("disable", name) == 0)
928 --- Start the given init script
929 -- @param name Name of the init script
930 -- @return Boolean indicating success
931 function init.start(name)
932 return (init_action("start", name) == 0)
935 --- Stop the given init script
936 -- @param name Name of the init script
937 -- @return Boolean indicating success
938 function init.stop(name)
939 return (init_action("stop", name) == 0)
943 -- Internal functions
945 function _parse_mixed_record(cnt, delimiter)
946 delimiter = delimiter or " "
950 for i, l in pairs(luci.util.split(luci.util.trim(cnt), "\n")) do
951 for j, f in pairs(luci.util.split(luci.util.trim(l), delimiter, nil, true)) do
952 local k, x, v = f:match('([^%s][^:=]*) *([:=]*) *"*([^\n"]*)"*')
956 table.insert(flags, k)