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 luci.util.pcdata(fs.readfile("/tmp/sysinfo/model")) or
190 cpuinfo:match("machine\t+: ([^\n]+)") or
191 cpuinfo:match("Hardware\t+: ([^\n]+)") or
192 luci.util.pcdata(fs.readfile("/proc/diag/model")) or
193 nixio.uname().machine or
196 return system, model, memtotal, memcached, membuffers, memfree, bogomips, swaptotal, swapcached, swapfree
199 --- Retrieves the output of the "logread" command.
200 -- @return String containing the current log buffer
202 return luci.util.exec("logread")
205 --- Retrieves the output of the "dmesg" command.
206 -- @return String containing the current log buffer
208 return luci.util.exec("dmesg")
211 --- Generates a random id with specified length.
212 -- @param bytes Number of bytes for the unique id
213 -- @return String containing hex encoded id
214 function uniqueid(bytes)
215 local rand = fs.readfile("/dev/urandom", bytes)
216 return rand and nixio.bin.hexlify(rand)
219 --- Returns the current system uptime stats.
220 -- @return String containing total uptime in seconds
222 return nixio.sysinfo().uptime
226 --- LuCI system utilities / network related functions.
228 -- @name luci.sys.net
231 --- Returns the current arp-table entries as two-dimensional table.
232 -- @return Table of table containing the current arp entries.
233 -- The following fields are defined for arp entry objects:
234 -- { "IP address", "HW address", "HW type", "Flags", "Mask", "Device" }
235 function net.arptable(callback)
237 if fs.access("/proc/net/arp") then
238 for e in io.lines("/proc/net/arp") do
240 for v in e:gmatch("%S+") do
246 ["IP address"] = r[1],
249 ["HW address"] = r[4],
266 local function _nethints(what, callback)
267 local _, k, e, mac, ip, name
268 local cur = uci.cursor()
272 local function _add(i, ...)
273 local k = select(i, ...)
275 if not hosts[k] then hosts[k] = { } end
276 hosts[k][1] = select(1, ...) or hosts[k][1]
277 hosts[k][2] = select(2, ...) or hosts[k][2]
278 hosts[k][3] = select(3, ...) or hosts[k][3]
279 hosts[k][4] = select(4, ...) or hosts[k][4]
283 if fs.access("/proc/net/arp") then
284 for e in io.lines("/proc/net/arp") do
285 ip, mac = e:match("^([%d%.]+)%s+%S+%s+%S+%s+([a-fA-F0-9:]+)%s+")
287 _add(what, mac:upper(), ip, nil, nil)
292 if fs.access("/etc/ethers") then
293 for e in io.lines("/etc/ethers") do
294 mac, ip = e:match("^([a-f0-9]%S+) (%S+)")
296 _add(what, mac:upper(), ip, nil, nil)
301 if fs.access("/var/dhcp.leases") then
302 for e in io.lines("/var/dhcp.leases") do
303 mac, ip, name = e:match("^%d+ (%S+) (%S+) (%S+)")
305 _add(what, mac:upper(), ip, nil, name ~= "*" and name)
310 cur:foreach("dhcp", "host",
312 for mac in luci.util.imatch(s.mac) do
313 _add(what, mac:upper(), s.ip, nil, s.name)
317 for _, e in ipairs(nixio.getifaddrs()) do
318 if e.name ~= "lo" then
319 ifn[e.name] = ifn[e.name] or { }
320 if e.family == "packet" and e.addr and #e.addr == 17 then
321 ifn[e.name][1] = e.addr:upper()
322 elseif e.family == "inet" then
323 ifn[e.name][2] = e.addr
324 elseif e.family == "inet6" then
325 ifn[e.name][3] = e.addr
330 for _, e in pairs(ifn) do
331 if e[what] and (e[2] or e[3]) then
332 _add(what, e[1], e[2], e[3], e[4])
336 for _, e in luci.util.kspairs(hosts) do
337 callback(e[1], e[2], e[3], e[4])
341 --- Returns a two-dimensional table of mac address hints.
342 -- @return Table of table containing known hosts from various sources.
343 -- Each entry contains the values in the following order:
345 function net.mac_hints(callback)
347 _nethints(1, function(mac, v4, v6, name)
348 name = name or nixio.getnameinfo(v4 or v6, nil, 100) or v4
349 if name and name ~= mac then
350 callback(mac, name or nixio.getnameinfo(v4 or v6, nil, 100) or v4)
355 _nethints(1, function(mac, v4, v6, name)
356 name = name or nixio.getnameinfo(v4 or v6, nil, 100) or v4
357 if name and name ~= mac then
358 rv[#rv+1] = { mac, name or nixio.getnameinfo(v4 or v6, nil, 100) or v4 }
365 --- Returns a two-dimensional table of IPv4 address hints.
366 -- @return Table of table containing known hosts from various sources.
367 -- Each entry contains the values in the following order:
369 function net.ipv4_hints(callback)
371 _nethints(2, function(mac, v4, v6, name)
372 name = name or nixio.getnameinfo(v4, nil, 100) or mac
373 if name and name ~= v4 then
379 _nethints(2, function(mac, v4, v6, name)
380 name = name or nixio.getnameinfo(v4, nil, 100) or mac
381 if name and name ~= v4 then
382 rv[#rv+1] = { v4, name }
389 --- Returns a two-dimensional table of IPv6 address hints.
390 -- @return Table of table containing known hosts from various sources.
391 -- Each entry contains the values in the following order:
393 function net.ipv6_hints(callback)
395 _nethints(3, function(mac, v4, v6, name)
396 name = name or nixio.getnameinfo(v6, nil, 100) or mac
397 if name and name ~= v6 then
403 _nethints(3, function(mac, v4, v6, name)
404 name = name or nixio.getnameinfo(v6, nil, 100) or mac
405 if name and name ~= v6 then
406 rv[#rv+1] = { v6, name }
413 --- Returns conntrack information
414 -- @return Table with the currently tracked IP connections
415 function net.conntrack(callback)
417 if fs.access("/proc/net/nf_conntrack", "r") then
418 for line in io.lines("/proc/net/nf_conntrack") do
419 line = line:match "^(.-( [^ =]+=).-)%2"
420 local entry, flags = _parse_mixed_record(line, " +")
421 if flags[6] ~= "TIME_WAIT" then
422 entry.layer3 = flags[1]
423 entry.layer4 = flags[3]
431 connt[#connt+1] = entry
435 elseif fs.access("/proc/net/ip_conntrack", "r") then
436 for line in io.lines("/proc/net/ip_conntrack") do
437 line = line:match "^(.-( [^ =]+=).-)%2"
438 local entry, flags = _parse_mixed_record(line, " +")
439 if flags[4] ~= "TIME_WAIT" then
440 entry.layer3 = "ipv4"
441 entry.layer4 = flags[1]
449 connt[#connt+1] = entry
459 --- Determine the current IPv4 default route. If multiple default routes exist,
460 -- return the one with the lowest metric.
461 -- @return Table with the properties of the current default route.
462 -- The following fields are defined:
463 -- { "dest", "gateway", "metric", "refcount", "usecount", "irtt",
464 -- "flags", "device" }
465 function net.defaultroute()
468 net.routes(function(rt)
469 if rt.dest:prefix() == 0 and (not route or route.metric > rt.metric) then
477 --- Determine the current IPv6 default route. If multiple default routes exist,
478 -- return the one with the lowest metric.
479 -- @return Table with the properties of the current default route.
480 -- The following fields are defined:
481 -- { "source", "dest", "nexthop", "metric", "refcount", "usecount",
482 -- "flags", "device" }
483 function net.defaultroute6()
486 net.routes6(function(rt)
487 if rt.dest:prefix() == 0 and rt.device ~= "lo" and
488 (not route or route.metric > rt.metric)
495 local global_unicast = luci.ip.IPv6("2000::/3")
496 net.routes6(function(rt)
497 if rt.dest:equal(global_unicast) and
498 (not route or route.metric > rt.metric)
508 --- Determine the names of available network interfaces.
509 -- @return Table containing all current interface names
510 function net.devices()
512 for k, v in ipairs(nixio.getifaddrs()) do
513 if v.family == "packet" then
514 devs[#devs+1] = v.name
521 --- Return information about available network interfaces.
522 -- @return Table containing all current interface names and their information
523 function net.deviceinfo()
525 for k, v in ipairs(nixio.getifaddrs()) do
526 if v.family == "packet" then
551 -- Determine the MAC address belonging to the given IP address.
552 -- @param ip IPv4 address
553 -- @return String containing the MAC address or nil if it cannot be found
554 function net.ip4mac(ip)
556 net.arptable(function(e)
557 if e["IP address"] == ip then
558 mac = e["HW address"]
564 --- Returns the current kernel routing table entries.
565 -- @return Table of tables with properties of the corresponding routes.
566 -- The following fields are defined for route entry tables:
567 -- { "dest", "gateway", "metric", "refcount", "usecount", "irtt",
568 -- "flags", "device" }
569 function net.routes(callback)
572 for line in io.lines("/proc/net/route") do
574 local dev, dst_ip, gateway, flags, refcnt, usecnt, metric,
575 dst_mask, mtu, win, irtt = line:match(
576 "([^%s]+)\t([A-F0-9]+)\t([A-F0-9]+)\t([A-F0-9]+)\t" ..
577 "(%d+)\t(%d+)\t(%d+)\t([A-F0-9]+)\t(%d+)\t(%d+)\t(%d+)"
581 gateway = luci.ip.Hex( gateway, 32, luci.ip.FAMILY_INET4 )
582 dst_mask = luci.ip.Hex( dst_mask, 32, luci.ip.FAMILY_INET4 )
583 dst_ip = luci.ip.Hex(
584 dst_ip, dst_mask:prefix(dst_mask), luci.ip.FAMILY_INET4
590 metric = tonumber(metric),
591 refcount = tonumber(refcnt),
592 usecount = tonumber(usecnt),
594 window = tonumber(window),
595 irtt = tonumber(irtt),
596 flags = tonumber(flags, 16),
603 routes[#routes+1] = rt
611 --- Returns the current ipv6 kernel routing table entries.
612 -- @return Table of tables with properties of the corresponding routes.
613 -- The following fields are defined for route entry tables:
614 -- { "source", "dest", "nexthop", "metric", "refcount", "usecount",
615 -- "flags", "device" }
616 function net.routes6(callback)
617 if fs.access("/proc/net/ipv6_route", "r") then
620 for line in io.lines("/proc/net/ipv6_route") do
622 local dst_ip, dst_prefix, src_ip, src_prefix, nexthop,
623 metric, refcnt, usecnt, flags, dev = line:match(
624 "([a-f0-9]+) ([a-f0-9]+) " ..
625 "([a-f0-9]+) ([a-f0-9]+) " ..
626 "([a-f0-9]+) ([a-f0-9]+) " ..
627 "([a-f0-9]+) ([a-f0-9]+) " ..
628 "([a-f0-9]+) +([^%s]+)"
631 if dst_ip and dst_prefix and
632 src_ip and src_prefix and
633 nexthop and metric and
634 refcnt and usecnt and
637 src_ip = luci.ip.Hex(
638 src_ip, tonumber(src_prefix, 16), luci.ip.FAMILY_INET6, false
641 dst_ip = luci.ip.Hex(
642 dst_ip, tonumber(dst_prefix, 16), luci.ip.FAMILY_INET6, false
645 nexthop = luci.ip.Hex( nexthop, 128, luci.ip.FAMILY_INET6, false )
651 metric = tonumber(metric, 16),
652 refcount = tonumber(refcnt, 16),
653 usecount = tonumber(usecnt, 16),
654 flags = tonumber(flags, 16),
657 -- lua number is too small for storing the metric
658 -- add a metric_raw field with the original content
665 routes[#routes+1] = rt
674 --- Tests whether the given host responds to ping probes.
675 -- @param host String containing a hostname or IPv4 address
676 -- @return Number containing 0 on success and >= 1 on error
677 function net.pingtest(host)
678 return os.execute("ping -c1 '"..host:gsub("'", '').."' >/dev/null 2>&1")
682 --- LuCI system utilities / process related functions.
684 -- @name luci.sys.process
687 --- Get the current process id.
689 -- @name process.info
690 -- @return Number containing the current pid
691 function process.info(key)
692 local s = {uid = nixio.getuid(), gid = nixio.getgid()}
693 return not key and s or s[key]
696 --- Retrieve information about currently running processes.
697 -- @return Table containing process information
698 function process.list()
701 local ps = luci.util.execi("/bin/busybox top -bn1")
708 local pid, ppid, user, stat, vsz, mem, cpu, cmd = line:match(
709 "^ *(%d+) +(%d+) +(%S.-%S) +([RSDZTW][W ][<N ]) +(%d+) +(%d+%%) +(%d+%%) +(.+)"
712 local idx = tonumber(pid)
730 --- Set the gid of a process identified by given pid.
731 -- @param gid Number containing the Unix group id
732 -- @return Boolean indicating successful operation
733 -- @return String containing the error message if failed
734 -- @return Number containing the error code if failed
735 function process.setgroup(gid)
736 return nixio.setgid(gid)
739 --- Set the uid of a process identified by given pid.
740 -- @param uid Number containing the Unix user id
741 -- @return Boolean indicating successful operation
742 -- @return String containing the error message if failed
743 -- @return Number containing the error code if failed
744 function process.setuser(uid)
745 return nixio.setuid(uid)
748 --- Send a signal to a process identified by given pid.
750 -- @name process.signal
751 -- @param pid Number containing the process id
752 -- @param sig Signal to send (default: 15 [SIGTERM])
753 -- @return Boolean indicating successful operation
754 -- @return Number containing the error code if failed
755 process.signal = nixio.kill
758 --- LuCI system utilities / user related functions.
760 -- @name luci.sys.user
763 --- Retrieve user informations for given uid.
766 -- @param uid Number containing the Unix user id
767 -- @return Table containing the following fields:
768 -- { "uid", "gid", "name", "passwd", "dir", "shell", "gecos" }
769 user.getuser = nixio.getpw
771 --- Retrieve the current user password hash.
772 -- @param username String containing the username to retrieve the password for
773 -- @return String containing the hash or nil if no password is set.
774 -- @return Password database entry
775 function user.getpasswd(username)
776 local pwe = nixio.getsp and nixio.getsp(username) or nixio.getpw(username)
777 local pwh = pwe and (pwe.pwdp or pwe.passwd)
778 if not pwh or #pwh < 1 or pwh == "!" or pwh == "x" then
785 --- Test whether given string matches the password of a given system user.
786 -- @param username String containing the Unix user name
787 -- @param pass String containing the password to compare
788 -- @return Boolean indicating wheather the passwords are equal
789 function user.checkpasswd(username, pass)
790 local pwh, pwe = user.getpasswd(username)
792 return (pwh == nil or nixio.crypt(pass, pwh) == pwh)
797 --- Change the password of given user.
798 -- @param username String containing the Unix user name
799 -- @param password String containing the password to compare
800 -- @return Number containing 0 on success and >= 1 on error
801 function user.setpasswd(username, password)
803 password = password:gsub("'", [['"'"']])
807 username = username:gsub("'", [['"'"']])
811 "(echo '" .. password .. "'; sleep 1; echo '" .. password .. "') | " ..
812 "passwd '" .. username .. "' >/dev/null 2>&1"
817 --- LuCI system utilities / wifi related functions.
819 -- @name luci.sys.wifi
822 --- Get wireless information for given interface.
823 -- @param ifname String containing the interface name
824 -- @return A wrapped iwinfo object instance
825 function wifi.getiwinfo(ifname)
826 local stat, iwinfo = pcall(require, "iwinfo")
830 local u = uci.cursor_state()
831 local d, n = ifname:match("^(%w+)%.network(%d+)")
835 u:foreach("wireless", "wifi-iface",
837 if s.device == d then
840 ifname = s.ifname or s.device
845 elseif u:get("wireless", ifname) == "wifi-device" then
846 u:foreach("wireless", "wifi-iface",
848 if s.device == ifname and s.ifname then
855 local t = stat and iwinfo.type(ifname)
856 local x = t and iwinfo[t] or { }
857 return setmetatable({}, {
858 __index = function(t, k)
859 if k == "ifname" then
870 --- LuCI system utilities / init related functions.
872 -- @name luci.sys.init
874 init.dir = "/etc/init.d/"
876 --- Get the names of all installed init scripts
877 -- @return Table containing the names of all inistalled init scripts
878 function init.names()
880 for name in fs.glob(init.dir.."*") do
881 names[#names+1] = fs.basename(name)
886 --- Get the index of he given init script
887 -- @param name Name of the init script
888 -- @return Numeric index value
889 function init.index(name)
890 if fs.access(init.dir..name) then
891 return call("env -i sh -c 'source %s%s enabled; exit ${START:-255}' >/dev/null"
896 local function init_action(action, name)
897 if fs.access(init.dir..name) then
898 return call("env -i %s%s %s >/dev/null" %{ init.dir, name, action })
902 --- Test whether the given init script is enabled
903 -- @param name Name of the init script
904 -- @return Boolean indicating whether init is enabled
905 function init.enabled(name)
906 return (init_action("enabled", name) == 0)
909 --- Enable the given init script
910 -- @param name Name of the init script
911 -- @return Boolean indicating success
912 function init.enable(name)
913 return (init_action("enable", name) == 1)
916 --- Disable the given init script
917 -- @param name Name of the init script
918 -- @return Boolean indicating success
919 function init.disable(name)
920 return (init_action("disable", name) == 0)
923 --- Start the given init script
924 -- @param name Name of the init script
925 -- @return Boolean indicating success
926 function init.start(name)
927 return (init_action("start", name) == 0)
930 --- Stop the given init script
931 -- @param name Name of the init script
932 -- @return Boolean indicating success
933 function init.stop(name)
934 return (init_action("stop", name) == 0)
938 -- Internal functions
940 function _parse_mixed_record(cnt, delimiter)
941 delimiter = delimiter or " "
945 for i, l in pairs(luci.util.split(luci.util.trim(cnt), "\n")) do
946 for j, f in pairs(luci.util.split(luci.util.trim(l), delimiter, nil, true)) do
947 local k, x, v = f:match('([^%s][^:=]*) *([:=]*) *"*([^\n"]*)"*')
951 table.insert(flags, k)