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 =
40 tonumber, ipairs, pairs, pcall, type, next, setmetatable, require
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
170 local cpuinfo = fs.readfile("/proc/cpuinfo")
171 local meminfo = fs.readfile("/proc/meminfo")
173 local system = cpuinfo:match("system typ.-:%s*([^\n]+)")
175 local memtotal = tonumber(meminfo:match("MemTotal:%s*(%d+)"))
176 local memcached = tonumber(meminfo:match("\nCached:%s*(%d+)"))
177 local memfree = tonumber(meminfo:match("MemFree:%s*(%d+)"))
178 local membuffers = tonumber(meminfo:match("Buffers:%s*(%d+)"))
181 system = nixio.uname().machine
182 model = cpuinfo:match("model name.-:%s*([^\n]+)")
184 model = cpuinfo:match("Processor.-:%s*([^\n]+)")
187 model = cpuinfo:match("cpu model.-:%s*([^\n]+)")
190 return system, model, memtotal, memcached, membuffers, memfree
193 --- Retrieves the output of the "logread" command.
194 -- @return String containing the current log buffer
196 return luci.util.exec("logread")
199 --- Retrieves the output of the "dmesg" command.
200 -- @return String containing the current log buffer
202 return luci.util.exec("dmesg")
205 --- Generates a random id with specified length.
206 -- @param bytes Number of bytes for the unique id
207 -- @return String containing hex encoded id
208 function uniqueid(bytes)
209 local rand = fs.readfile("/dev/urandom", bytes)
210 return rand and nixio.bin.hexlify(rand)
213 --- Returns the current system uptime stats.
214 -- @return String containing total uptime in seconds
216 return nixio.sysinfo().uptime
220 --- LuCI system utilities / network related functions.
222 -- @name luci.sys.net
225 --- Returns the current arp-table entries as two-dimensional table.
226 -- @return Table of table containing the current arp entries.
227 -- The following fields are defined for arp entry objects:
228 -- { "IP address", "HW address", "HW type", "Flags", "Mask", "Device" }
229 function net.arptable(callback)
230 return _parse_delimited_table(io.lines("/proc/net/arp"), "%s%s+", callback)
233 --- Returns conntrack information
234 -- @return Table with the currently tracked IP connections
235 function net.conntrack(callback)
237 if fs.access("/proc/net/nf_conntrack", "r") then
238 for line in io.lines("/proc/net/nf_conntrack") do
239 line = line:match "^(.-( [^ =]+=).-)%2"
240 local entry, flags = _parse_mixed_record(line, " +")
241 entry.layer3 = flags[1]
242 entry.layer4 = flags[3]
250 connt[#connt+1] = entry
253 elseif fs.access("/proc/net/ip_conntrack", "r") then
254 for line in io.lines("/proc/net/ip_conntrack") do
255 line = line:match "^(.-( [^ =]+=).-)%2"
256 local entry, flags = _parse_mixed_record(line, " +")
257 entry.layer3 = "ipv4"
258 entry.layer4 = flags[1]
266 connt[#connt+1] = entry
275 --- Determine the current IPv4 default route. If multiple default routes exist,
276 -- return the one with the lowest metric.
277 -- @return Table with the properties of the current default route.
278 -- The following fields are defined:
279 -- { "dest", "gateway", "metric", "refcount", "usecount", "irtt",
280 -- "flags", "device" }
281 function net.defaultroute()
284 net.routes(function(rt)
285 if rt.dest:prefix() == 0 and (not route or route.metric > rt.metric) then
293 --- Determine the current IPv6 default route. If multiple default routes exist,
294 -- return the one with the lowest metric.
295 -- @return Table with the properties of the current default route.
296 -- The following fields are defined:
297 -- { "source", "dest", "nexthop", "metric", "refcount", "usecount",
298 -- "flags", "device" }
299 function net.defaultroute6()
302 net.routes6(function(rt)
303 if rt.dest:prefix() == 0 and (not route or route.metric > rt.metric) then
311 --- Determine the names of available network interfaces.
312 -- @return Table containing all current interface names
313 function net.devices()
315 for k, v in ipairs(nixio.getifaddrs()) do
316 if v.family == "packet" then
317 devs[#devs+1] = v.name
324 --- Return information about available network interfaces.
325 -- @return Table containing all current interface names and their information
326 function net.deviceinfo()
328 for k, v in ipairs(nixio.getifaddrs()) do
329 if v.family == "packet" then
354 -- Determine the MAC address belonging to the given IP address.
355 -- @param ip IPv4 address
356 -- @return String containing the MAC address or nil if it cannot be found
357 function net.ip4mac(ip)
359 net.arptable(function(e)
360 if e["IP address"] == ip then
361 mac = e["HW address"]
367 --- Returns the current kernel routing table entries.
368 -- @return Table of tables with properties of the corresponding routes.
369 -- The following fields are defined for route entry tables:
370 -- { "dest", "gateway", "metric", "refcount", "usecount", "irtt",
371 -- "flags", "device" }
372 function net.routes(callback)
375 for line in io.lines("/proc/net/route") do
377 local dev, dst_ip, gateway, flags, refcnt, usecnt, metric,
378 dst_mask, mtu, win, irtt = line:match(
379 "([^%s]+)\t([A-F0-9]+)\t([A-F0-9]+)\t([A-F0-9]+)\t" ..
380 "(%d+)\t(%d+)\t(%d+)\t([A-F0-9]+)\t(%d+)\t(%d+)\t(%d+)"
384 gateway = luci.ip.Hex( gateway, 32, luci.ip.FAMILY_INET4 )
385 dst_mask = luci.ip.Hex( dst_mask, 32, luci.ip.FAMILY_INET4 )
386 dst_ip = luci.ip.Hex(
387 dst_ip, dst_mask:prefix(dst_mask), luci.ip.FAMILY_INET4
393 metric = tonumber(metric),
394 refcount = tonumber(refcnt),
395 usecount = tonumber(usecnt),
397 window = tonumber(window),
398 irtt = tonumber(irtt),
399 flags = tonumber(flags, 16),
406 routes[#routes+1] = rt
414 --- Returns the current ipv6 kernel routing table entries.
415 -- @return Table of tables with properties of the corresponding routes.
416 -- The following fields are defined for route entry tables:
417 -- { "source", "dest", "nexthop", "metric", "refcount", "usecount",
418 -- "flags", "device" }
419 function net.routes6(callback)
420 if fs.access("/proc/net/ipv6_route", "r") then
423 for line in io.lines("/proc/net/ipv6_route") do
425 local dst_ip, dst_prefix, src_ip, src_prefix, nexthop,
426 metric, refcnt, usecnt, flags, dev = line:match(
427 "([a-f0-9]+) ([a-f0-9]+) " ..
428 "([a-f0-9]+) ([a-f0-9]+) " ..
429 "([a-f0-9]+) ([a-f0-9]+) " ..
430 "([a-f0-9]+) ([a-f0-9]+) " ..
431 "([a-f0-9]+) +([^%s]+)"
434 src_ip = luci.ip.Hex(
435 src_ip, tonumber(src_prefix, 16), luci.ip.FAMILY_INET6, false
438 dst_ip = luci.ip.Hex(
439 dst_ip, tonumber(dst_prefix, 16), luci.ip.FAMILY_INET6, false
442 nexthop = luci.ip.Hex( nexthop, 128, luci.ip.FAMILY_INET6, false )
448 metric = tonumber(metric, 16),
449 refcount = tonumber(refcnt, 16),
450 usecount = tonumber(usecnt, 16),
451 flags = tonumber(flags, 16),
454 -- lua number is too small for storing the metric
455 -- add a metric_raw field with the original content
462 routes[#routes+1] = rt
470 --- Tests whether the given host responds to ping probes.
471 -- @param host String containing a hostname or IPv4 address
472 -- @return Number containing 0 on success and >= 1 on error
473 function net.pingtest(host)
474 return os.execute("ping -c1 '"..host:gsub("'", '').."' >/dev/null 2>&1")
478 --- LuCI system utilities / process related functions.
480 -- @name luci.sys.process
483 --- Get the current process id.
485 -- @name process.info
486 -- @return Number containing the current pid
487 function process.info(key)
488 local s = {uid = nixio.getuid(), gid = nixio.getgid()}
489 return not key and s or s[key]
492 --- Retrieve information about currently running processes.
493 -- @return Table containing process information
494 function process.list()
497 local ps = luci.util.execi("top -bn1")
509 k = luci.util.split(luci.util.trim(line), "%s+", nil, true)
510 if k[1] == "PID" then
518 line = luci.util.trim(line)
519 for i, value in ipairs(luci.util.split(line, "%s+", #k-1, true)) do
523 local pid = tonumber(row[k[1]])
532 --- Set the gid of a process identified by given pid.
533 -- @param gid Number containing the Unix group id
534 -- @return Boolean indicating successful operation
535 -- @return String containing the error message if failed
536 -- @return Number containing the error code if failed
537 function process.setgroup(gid)
538 return nixio.setgid(gid)
541 --- Set the uid of a process identified by given pid.
542 -- @param uid Number containing the Unix user id
543 -- @return Boolean indicating successful operation
544 -- @return String containing the error message if failed
545 -- @return Number containing the error code if failed
546 function process.setuser(uid)
547 return nixio.setuid(uid)
550 --- Send a signal to a process identified by given pid.
552 -- @name process.signal
553 -- @param pid Number containing the process id
554 -- @param sig Signal to send (default: 15 [SIGTERM])
555 -- @return Boolean indicating successful operation
556 -- @return Number containing the error code if failed
557 process.signal = nixio.kill
560 --- LuCI system utilities / user related functions.
562 -- @name luci.sys.user
565 --- Retrieve user informations for given uid.
568 -- @param uid Number containing the Unix user id
569 -- @return Table containing the following fields:
570 -- { "uid", "gid", "name", "passwd", "dir", "shell", "gecos" }
571 user.getuser = nixio.getpw
573 --- Retrieve the current user password hash.
574 -- @param username String containing the username to retrieve the password for
575 -- @return String containing the hash or nil if no password is set.
576 function user.getpasswd(username)
577 local pwe = nixio.getsp and nixio.getsp(username) or nixio.getpw(username)
578 local pwh = pwe and (pwe.pwdp or pwe.passwd)
579 if not pwh or #pwh < 1 or pwh == "!" or pwh == "x" then
586 --- Test whether given string matches the password of a given system user.
587 -- @param username String containing the Unix user name
588 -- @param pass String containing the password to compare
589 -- @return Boolean indicating wheather the passwords are equal
590 function user.checkpasswd(username, pass)
591 local pwh = user.getpasswd(username)
592 if pwh and nixio.crypt(pass, pwh) ~= pwh then
599 --- Change the password of given user.
600 -- @param username String containing the Unix user name
601 -- @param password String containing the password to compare
602 -- @return Number containing 0 on success and >= 1 on error
603 function user.setpasswd(username, password)
605 password = password:gsub("'", "")
609 username = username:gsub("'", "")
612 local cmd = "(echo '"..password.."';sleep 1;echo '"..password.."')|"
613 cmd = cmd .. "passwd '"..username.."' >/dev/null 2>&1"
614 return os.execute(cmd)
618 --- LuCI system utilities / wifi related functions.
620 -- @name luci.sys.wifi
623 --- Get wireless information for given interface.
624 -- @param ifname String containing the interface name
625 -- @return A wrapped iwinfo object instance
626 function wifi.getiwinfo(ifname)
627 local stat, iwinfo = pcall(require, "iwinfo")
631 local u = uci.cursor_state()
632 local d, n = ifname:match("^(%w+)%.network(%d+)")
635 u:foreach("wireless", "wifi-iface",
637 if s.device == d then
640 ifname = s.ifname or s.device
645 elseif u:get("wireless", ifname) == "wifi-device" then
646 u:foreach("wireless", "wifi-iface",
648 if s.device == ifname and s.ifname then
655 local t = stat and iwinfo.type(ifname)
656 local x = t and iwinfo[t] or { }
657 return setmetatable({}, {
658 __index = function(t, k)
659 if k == "ifname" then
669 --- Get iwconfig output for all wireless devices.
670 -- @return Table of tables containing the iwconfing output for each wifi device
671 function wifi.getiwconfig()
672 local cnt = luci.util.exec("PATH=/sbin:/usr/sbin iwconfig 2>/dev/null")
675 for i, l in pairs(luci.util.split(luci.util.trim(cnt), "\n\n")) do
676 local k = l:match("^(.-) ")
677 l = l:gsub("^(.-) +", "", 1)
679 local entry, flags = _parse_mixed_record(l)
690 --- Get iwlist scan output from all wireless devices.
691 -- @return Table of tables contaiing all scan results
692 function wifi.iwscan(iface)
693 local siface = iface or ""
694 local cnt = luci.util.exec("iwlist "..siface.." scan 2>/dev/null")
697 for i, l in pairs(luci.util.split(luci.util.trim(cnt), "\n\n")) do
698 local k = l:match("^(.-) ")
699 l = l:gsub("^[^\n]+", "", 1)
700 l = luci.util.trim(l)
703 for j, c in pairs(luci.util.split(l, "\n Cell")) do
704 c = c:gsub("^(.-)- ", "", 1)
705 c = luci.util.split(c, "\n", 7)
706 c = table.concat(c, "\n", 1)
707 local entry, flags = _parse_mixed_record(c)
711 table.insert(iws[k], entry)
716 return iface and (iws[iface] or {}) or iws
719 --- Get available channels from given wireless iface.
720 -- @param iface Wireless interface (optional)
721 -- @return Table of available channels
722 function wifi.channels(iface)
723 local t = iwinfo.type(iface or "")
725 if iface and t and iwinfo[t] then
726 cns = iwinfo[t].freqlist(iface)
729 if not cns or #cns == 0 then
731 {channel = 1, mhz = 2412},
732 {channel = 2, mhz = 2417},
733 {channel = 3, mhz = 2422},
734 {channel = 4, mhz = 2427},
735 {channel = 5, mhz = 2432},
736 {channel = 6, mhz = 2437},
737 {channel = 7, mhz = 2442},
738 {channel = 8, mhz = 2447},
739 {channel = 9, mhz = 2452},
740 {channel = 10, mhz = 2457},
741 {channel = 11, mhz = 2462}
749 --- LuCI system utilities / init related functions.
751 -- @name luci.sys.init
753 init.dir = "/etc/init.d/"
755 --- Get the names of all installed init scripts
756 -- @return Table containing the names of all inistalled init scripts
757 function init.names()
759 for name in fs.glob(init.dir.."*") do
760 names[#names+1] = fs.basename(name)
765 --- Test whether the given init script is enabled
766 -- @param name Name of the init script
767 -- @return Boolean indicating whether init is enabled
768 function init.enabled(name)
769 if fs.access(init.dir..name) then
770 return ( call(init.dir..name.." enabled") == 0 )
775 --- Get the index of he given init script
776 -- @param name Name of the init script
777 -- @return Numeric index value
778 function init.index(name)
779 if fs.access(init.dir..name) then
780 return call("source "..init.dir..name.." enabled; exit $START")
784 --- Enable the given init script
785 -- @param name Name of the init script
786 -- @return Boolean indicating success
787 function init.enable(name)
788 if fs.access(init.dir..name) then
789 return ( call(init.dir..name.." enable") == 1 )
793 --- Disable the given init script
794 -- @param name Name of the init script
795 -- @return Boolean indicating success
796 function init.disable(name)
797 if fs.access(init.dir..name) then
798 return ( call(init.dir..name.." disable") == 0 )
803 -- Internal functions
805 function _parse_delimited_table(iter, delimiter, callback)
806 delimiter = delimiter or "%s+"
809 local trim = luci.util.trim
810 local split = luci.util.split
812 local keys = split(trim(iter()), delimiter, nil, true)
813 for i, j in pairs(keys) do
814 keys[i] = trim(keys[i])
821 for i, j in pairs(split(line, delimiter, nil, true)) do
838 function _parse_mixed_record(cnt, delimiter)
839 delimiter = delimiter or " "
843 for i, l in pairs(luci.util.split(luci.util.trim(cnt), "\n")) do
844 for j, f in pairs(luci.util.split(luci.util.trim(l), delimiter, nil, true)) do
845 local k, x, v = f:match('([^%s][^:=]*) *([:=]*) *"*([^\n"]*)"*')
849 table.insert(flags, k)