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
179 local swaptotal = tonumber(meminfo:match("SwapTotal:%s*(%d+)"))
180 local swapcached = tonumber(meminfo:match("SwapCached:%s*(%d+)"))
181 local swapfree = tonumber(meminfo:match("SwapFree:%s*(%d+)"))
184 cpuinfo:match("system type\t+: ([^\n]+)") or
185 cpuinfo:match("Processor\t+: ([^\n]+)") or
186 cpuinfo:match("model name\t+: ([^\n]+)")
189 fs.readfile("/proc/device-tree/model") or
190 luci.util.pcdata(fs.readfile("/tmp/sysinfo/model")) or
191 cpuinfo:match("machine\t+: ([^\n]+)") or
192 cpuinfo:match("Hardware\t+: ([^\n]+)") or
193 luci.util.pcdata(fs.readfile("/proc/diag/model")) or
194 nixio.uname().machine or
197 return system, model, memtotal, memcached, membuffers, memfree, bogomips, swaptotal, swapcached, swapfree
200 --- Retrieves the output of the "logread" command.
201 -- @return String containing the current log buffer
203 return luci.util.exec("logread")
206 --- Retrieves the output of the "dmesg" command.
207 -- @return String containing the current log buffer
209 return luci.util.exec("dmesg")
212 --- Generates a random id with specified length.
213 -- @param bytes Number of bytes for the unique id
214 -- @return String containing hex encoded id
215 function uniqueid(bytes)
216 local rand = fs.readfile("/dev/urandom", bytes)
217 return rand and nixio.bin.hexlify(rand)
220 --- Returns the current system uptime stats.
221 -- @return String containing total uptime in seconds
223 return nixio.sysinfo().uptime
227 --- LuCI system utilities / network related functions.
229 -- @name luci.sys.net
232 --- Returns the current arp-table entries as two-dimensional table.
233 -- @return Table of table containing the current arp entries.
234 -- The following fields are defined for arp entry objects:
235 -- { "IP address", "HW address", "HW type", "Flags", "Mask", "Device" }
236 function net.arptable(callback)
237 local arp = (not callback) and {} or nil
239 if fs.access("/proc/net/arp") then
240 for e in io.lines("/proc/net/arp") do
242 for v in e:gmatch("%S+") do
248 ["IP address"] = r[1],
251 ["HW address"] = r[4],
268 local function _nethints(what, callback)
269 local _, k, e, mac, ip, name
270 local cur = uci.cursor()
274 local function _add(i, ...)
275 local k = select(i, ...)
277 if not hosts[k] then hosts[k] = { } end
278 hosts[k][1] = select(1, ...) or hosts[k][1]
279 hosts[k][2] = select(2, ...) or hosts[k][2]
280 hosts[k][3] = select(3, ...) or hosts[k][3]
281 hosts[k][4] = select(4, ...) or hosts[k][4]
285 if fs.access("/proc/net/arp") then
286 for e in io.lines("/proc/net/arp") do
287 ip, mac = e:match("^([%d%.]+)%s+%S+%s+%S+%s+([a-fA-F0-9:]+)%s+")
289 _add(what, mac:upper(), ip, nil, nil)
294 if fs.access("/etc/ethers") then
295 for e in io.lines("/etc/ethers") do
296 mac, ip = e:match("^([a-f0-9]%S+) (%S+)")
298 _add(what, mac:upper(), ip, nil, nil)
303 if fs.access("/var/dhcp.leases") then
304 for e in io.lines("/var/dhcp.leases") do
305 mac, ip, name = e:match("^%d+ (%S+) (%S+) (%S+)")
307 _add(what, mac:upper(), ip, nil, name ~= "*" and name)
312 cur:foreach("dhcp", "host",
314 for mac in luci.util.imatch(s.mac) do
315 _add(what, mac:upper(), s.ip, nil, s.name)
319 for _, e in ipairs(nixio.getifaddrs()) do
320 if e.name ~= "lo" then
321 ifn[e.name] = ifn[e.name] or { }
322 if e.family == "packet" and e.addr and #e.addr == 17 then
323 ifn[e.name][1] = e.addr:upper()
324 elseif e.family == "inet" then
325 ifn[e.name][2] = e.addr
326 elseif e.family == "inet6" then
327 ifn[e.name][3] = e.addr
332 for _, e in pairs(ifn) do
333 if e[what] and (e[2] or e[3]) then
334 _add(what, e[1], e[2], e[3], e[4])
338 for _, e in luci.util.kspairs(hosts) do
339 callback(e[1], e[2], e[3], e[4])
343 --- Returns a two-dimensional table of mac address hints.
344 -- @return Table of table containing known hosts from various sources.
345 -- Each entry contains the values in the following order:
347 function net.mac_hints(callback)
349 _nethints(1, function(mac, v4, v6, name)
350 name = name or nixio.getnameinfo(v4 or v6, nil, 100) or v4
351 if name and name ~= mac then
352 callback(mac, name or nixio.getnameinfo(v4 or v6, nil, 100) or v4)
357 _nethints(1, function(mac, v4, v6, name)
358 name = name or nixio.getnameinfo(v4 or v6, nil, 100) or v4
359 if name and name ~= mac then
360 rv[#rv+1] = { mac, name or nixio.getnameinfo(v4 or v6, nil, 100) or v4 }
367 --- Returns a two-dimensional table of IPv4 address hints.
368 -- @return Table of table containing known hosts from various sources.
369 -- Each entry contains the values in the following order:
371 function net.ipv4_hints(callback)
373 _nethints(2, function(mac, v4, v6, name)
374 name = name or nixio.getnameinfo(v4, nil, 100) or mac
375 if name and name ~= v4 then
381 _nethints(2, function(mac, v4, v6, name)
382 name = name or nixio.getnameinfo(v4, nil, 100) or mac
383 if name and name ~= v4 then
384 rv[#rv+1] = { v4, name }
391 --- Returns a two-dimensional table of IPv6 address hints.
392 -- @return Table of table containing known hosts from various sources.
393 -- Each entry contains the values in the following order:
395 function net.ipv6_hints(callback)
397 _nethints(3, function(mac, v4, v6, name)
398 name = name or nixio.getnameinfo(v6, nil, 100) or mac
399 if name and name ~= v6 then
405 _nethints(3, function(mac, v4, v6, name)
406 name = name or nixio.getnameinfo(v6, nil, 100) or mac
407 if name and name ~= v6 then
408 rv[#rv+1] = { v6, name }
415 --- Returns conntrack information
416 -- @return Table with the currently tracked IP connections
417 function net.conntrack(callback)
419 if fs.access("/proc/net/nf_conntrack", "r") then
420 for line in io.lines("/proc/net/nf_conntrack") do
421 line = line:match "^(.-( [^ =]+=).-)%2"
422 local entry, flags = _parse_mixed_record(line, " +")
423 if flags[6] ~= "TIME_WAIT" then
424 entry.layer3 = flags[1]
425 entry.layer4 = flags[3]
433 connt[#connt+1] = entry
437 elseif fs.access("/proc/net/ip_conntrack", "r") then
438 for line in io.lines("/proc/net/ip_conntrack") do
439 line = line:match "^(.-( [^ =]+=).-)%2"
440 local entry, flags = _parse_mixed_record(line, " +")
441 if flags[4] ~= "TIME_WAIT" then
442 entry.layer3 = "ipv4"
443 entry.layer4 = flags[1]
451 connt[#connt+1] = entry
461 --- Determine the current IPv4 default route. If multiple default routes exist,
462 -- return the one with the lowest metric.
463 -- @return Table with the properties of the current default route.
464 -- The following fields are defined:
465 -- { "dest", "gateway", "metric", "refcount", "usecount", "irtt",
466 -- "flags", "device" }
467 function net.defaultroute()
470 net.routes(function(rt)
471 if rt.dest:prefix() == 0 and (not route or route.metric > rt.metric) then
479 --- Determine the current IPv6 default route. If multiple default routes exist,
480 -- return the one with the lowest metric.
481 -- @return Table with the properties of the current default route.
482 -- The following fields are defined:
483 -- { "source", "dest", "nexthop", "metric", "refcount", "usecount",
484 -- "flags", "device" }
485 function net.defaultroute6()
488 net.routes6(function(rt)
489 if rt.dest:prefix() == 0 and rt.device ~= "lo" and
490 (not route or route.metric > rt.metric)
497 local global_unicast = luci.ip.IPv6("2000::/3")
498 net.routes6(function(rt)
499 if rt.dest:equal(global_unicast) and
500 (not route or route.metric > rt.metric)
510 --- Determine the names of available network interfaces.
511 -- @return Table containing all current interface names
512 function net.devices()
514 for k, v in ipairs(nixio.getifaddrs()) do
515 if v.family == "packet" then
516 devs[#devs+1] = v.name
523 --- Return information about available network interfaces.
524 -- @return Table containing all current interface names and their information
525 function net.deviceinfo()
527 for k, v in ipairs(nixio.getifaddrs()) do
528 if v.family == "packet" then
553 -- Determine the MAC address belonging to the given IP address.
554 -- @param ip IPv4 address
555 -- @return String containing the MAC address or nil if it cannot be found
556 function net.ip4mac(ip)
558 net.arptable(function(e)
559 if e["IP address"] == ip then
560 mac = e["HW address"]
566 --- Returns the current 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 -- { "dest", "gateway", "metric", "refcount", "usecount", "irtt",
570 -- "flags", "device" }
571 function net.routes(callback)
574 for line in io.lines("/proc/net/route") do
576 local dev, dst_ip, gateway, flags, refcnt, usecnt, metric,
577 dst_mask, mtu, win, irtt = line:match(
578 "([^%s]+)\t([A-F0-9]+)\t([A-F0-9]+)\t([A-F0-9]+)\t" ..
579 "(%d+)\t(%d+)\t(%d+)\t([A-F0-9]+)\t(%d+)\t(%d+)\t(%d+)"
583 gateway = luci.ip.Hex( gateway, 32, luci.ip.FAMILY_INET4 )
584 dst_mask = luci.ip.Hex( dst_mask, 32, luci.ip.FAMILY_INET4 )
585 dst_ip = luci.ip.Hex(
586 dst_ip, dst_mask:prefix(dst_mask), luci.ip.FAMILY_INET4
592 metric = tonumber(metric),
593 refcount = tonumber(refcnt),
594 usecount = tonumber(usecnt),
596 window = tonumber(window),
597 irtt = tonumber(irtt),
598 flags = tonumber(flags, 16),
605 routes[#routes+1] = rt
613 --- Returns the current ipv6 kernel routing table entries.
614 -- @return Table of tables with properties of the corresponding routes.
615 -- The following fields are defined for route entry tables:
616 -- { "source", "dest", "nexthop", "metric", "refcount", "usecount",
617 -- "flags", "device" }
618 function net.routes6(callback)
619 if fs.access("/proc/net/ipv6_route", "r") then
622 for line in io.lines("/proc/net/ipv6_route") do
624 local dst_ip, dst_prefix, src_ip, src_prefix, nexthop,
625 metric, refcnt, usecnt, flags, dev = line:match(
626 "([a-f0-9]+) ([a-f0-9]+) " ..
627 "([a-f0-9]+) ([a-f0-9]+) " ..
628 "([a-f0-9]+) ([a-f0-9]+) " ..
629 "([a-f0-9]+) ([a-f0-9]+) " ..
630 "([a-f0-9]+) +([^%s]+)"
633 if dst_ip and dst_prefix and
634 src_ip and src_prefix and
635 nexthop and metric and
636 refcnt and usecnt and
639 src_ip = luci.ip.Hex(
640 src_ip, tonumber(src_prefix, 16), luci.ip.FAMILY_INET6, false
643 dst_ip = luci.ip.Hex(
644 dst_ip, tonumber(dst_prefix, 16), luci.ip.FAMILY_INET6, false
647 nexthop = luci.ip.Hex( nexthop, 128, luci.ip.FAMILY_INET6, false )
653 metric = tonumber(metric, 16),
654 refcount = tonumber(refcnt, 16),
655 usecount = tonumber(usecnt, 16),
656 flags = tonumber(flags, 16),
659 -- lua number is too small for storing the metric
660 -- add a metric_raw field with the original content
667 routes[#routes+1] = rt
676 --- Tests whether the given host responds to ping probes.
677 -- @param host String containing a hostname or IPv4 address
678 -- @return Number containing 0 on success and >= 1 on error
679 function net.pingtest(host)
680 return os.execute("ping -c1 '"..host:gsub("'", '').."' >/dev/null 2>&1")
684 --- LuCI system utilities / process related functions.
686 -- @name luci.sys.process
689 --- Get the current process id.
691 -- @name process.info
692 -- @return Number containing the current pid
693 function process.info(key)
694 local s = {uid = nixio.getuid(), gid = nixio.getgid()}
695 return not key and s or s[key]
698 --- Retrieve information about currently running processes.
699 -- @return Table containing process information
700 function process.list()
703 local ps = luci.util.execi("/bin/busybox top -bn1")
710 local pid, ppid, user, stat, vsz, mem, cpu, cmd = line:match(
711 "^ *(%d+) +(%d+) +(%S.-%S) +([RSDZTW][W ][<N ]) +(%d+) +(%d+%%) +(%d+%%) +(.+)"
714 local idx = tonumber(pid)
732 --- Set the gid of a process identified by given pid.
733 -- @param gid Number containing the Unix group id
734 -- @return Boolean indicating successful operation
735 -- @return String containing the error message if failed
736 -- @return Number containing the error code if failed
737 function process.setgroup(gid)
738 return nixio.setgid(gid)
741 --- Set the uid of a process identified by given pid.
742 -- @param uid Number containing the Unix user id
743 -- @return Boolean indicating successful operation
744 -- @return String containing the error message if failed
745 -- @return Number containing the error code if failed
746 function process.setuser(uid)
747 return nixio.setuid(uid)
750 --- Send a signal to a process identified by given pid.
752 -- @name process.signal
753 -- @param pid Number containing the process id
754 -- @param sig Signal to send (default: 15 [SIGTERM])
755 -- @return Boolean indicating successful operation
756 -- @return Number containing the error code if failed
757 process.signal = nixio.kill
760 --- LuCI system utilities / user related functions.
762 -- @name luci.sys.user
765 --- Retrieve user informations for given uid.
768 -- @param uid Number containing the Unix user id
769 -- @return Table containing the following fields:
770 -- { "uid", "gid", "name", "passwd", "dir", "shell", "gecos" }
771 user.getuser = nixio.getpw
773 --- Retrieve the current user password hash.
774 -- @param username String containing the username to retrieve the password for
775 -- @return String containing the hash or nil if no password is set.
776 -- @return Password database entry
777 function user.getpasswd(username)
778 local pwe = nixio.getsp and nixio.getsp(username) or nixio.getpw(username)
779 local pwh = pwe and (pwe.pwdp or pwe.passwd)
780 if not pwh or #pwh < 1 or pwh == "!" or pwh == "x" then
787 --- Test whether given string matches the password of a given system user.
788 -- @param username String containing the Unix user name
789 -- @param pass String containing the password to compare
790 -- @return Boolean indicating wheather the passwords are equal
791 function user.checkpasswd(username, pass)
792 local pwh, pwe = user.getpasswd(username)
794 return (pwh == nil or nixio.crypt(pass, pwh) == pwh)
799 --- Change the password of given user.
800 -- @param username String containing the Unix user name
801 -- @param password String containing the password to compare
802 -- @return Number containing 0 on success and >= 1 on error
803 function user.setpasswd(username, password)
805 password = password:gsub("'", [['"'"']])
809 username = username:gsub("'", [['"'"']])
813 "(echo '" .. password .. "'; sleep 1; echo '" .. password .. "') | " ..
814 "passwd '" .. username .. "' >/dev/null 2>&1"
819 --- LuCI system utilities / wifi related functions.
821 -- @name luci.sys.wifi
824 --- Get wireless information for given interface.
825 -- @param ifname String containing the interface name
826 -- @return A wrapped iwinfo object instance
827 function wifi.getiwinfo(ifname)
828 local stat, iwinfo = pcall(require, "iwinfo")
832 local u = uci.cursor_state()
833 local d, n = ifname:match("^(%w+)%.network(%d+)")
837 u:foreach("wireless", "wifi-iface",
839 if s.device == d then
842 ifname = s.ifname or s.device
847 elseif u:get("wireless", ifname) == "wifi-device" then
848 u:foreach("wireless", "wifi-iface",
850 if s.device == ifname and s.ifname then
857 local t = stat and iwinfo.type(ifname)
858 local x = t and iwinfo[t] or { }
859 return setmetatable({}, {
860 __index = function(t, k)
861 if k == "ifname" then
872 --- LuCI system utilities / init related functions.
874 -- @name luci.sys.init
876 init.dir = "/etc/init.d/"
878 --- Get the names of all installed init scripts
879 -- @return Table containing the names of all inistalled init scripts
880 function init.names()
882 for name in fs.glob(init.dir.."*") do
883 names[#names+1] = fs.basename(name)
888 --- Get the index of he given init script
889 -- @param name Name of the init script
890 -- @return Numeric index value
891 function init.index(name)
892 if fs.access(init.dir..name) then
893 return call("env -i sh -c 'source %s%s enabled; exit ${START:-255}' >/dev/null"
898 local function init_action(action, name)
899 if fs.access(init.dir..name) then
900 return call("env -i %s%s %s >/dev/null" %{ init.dir, name, action })
904 --- Test whether the given init script is enabled
905 -- @param name Name of the init script
906 -- @return Boolean indicating whether init is enabled
907 function init.enabled(name)
908 return (init_action("enabled", name) == 0)
911 --- Enable the given init script
912 -- @param name Name of the init script
913 -- @return Boolean indicating success
914 function init.enable(name)
915 return (init_action("enable", name) == 1)
918 --- Disable the given init script
919 -- @param name Name of the init script
920 -- @return Boolean indicating success
921 function init.disable(name)
922 return (init_action("disable", name) == 0)
925 --- Start the given init script
926 -- @param name Name of the init script
927 -- @return Boolean indicating success
928 function init.start(name)
929 return (init_action("start", name) == 0)
932 --- Stop the given init script
933 -- @param name Name of the init script
934 -- @return Boolean indicating success
935 function init.stop(name)
936 return (init_action("stop", name) == 0)
940 -- Internal functions
942 function _parse_mixed_record(cnt, delimiter)
943 delimiter = delimiter or " "
947 for i, l in pairs(luci.util.split(luci.util.trim(cnt), "\n")) do
948 for j, f in pairs(luci.util.split(luci.util.trim(l), delimiter, nil, true)) do
949 local k, x, v = f:match('([^%s][^:=]*) *([:=]*) *"*([^\n"]*)"*')
953 table.insert(flags, k)