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 iwinfo = require "iwinfo"
34 local uci = require "luci.model.uci"
37 luci.util = require "luci.util"
38 luci.ip = require "luci.ip"
40 local tonumber, ipairs, pairs, pcall, type, next, setmetatable =
41 tonumber, ipairs, pairs, pcall, type, next, setmetatable
44 --- LuCI Linux and POSIX system utilities.
47 --- Execute a given shell command and return the error code
50 -- @param ... Command to call
51 -- @return Error code of the command
53 return os.execute(...) / 256
56 --- Execute a given shell command and capture its standard output
59 -- @param command Command to call
60 -- @return String containg the return the output of the command
63 --- Invoke the luci-flash executable to write an image to the flash memory.
64 -- @param image Local path or URL to image file
65 -- @param kpattern Pattern of files to keep over flash process
66 -- @return Return value of os.execute()
67 function flash(image, kpattern)
68 local cmd = "luci-flash "
70 cmd = cmd .. "-k '" .. kpattern:gsub("'", "") .. "' "
72 cmd = cmd .. "'" .. image:gsub("'", "") .. "' >/dev/null 2>&1"
74 return os.execute(cmd)
77 --- Retrieve information about currently mounted file systems.
78 -- @return Table containing mount information
81 local k = {"fs", "blocks", "used", "available", "percent", "mountpoint"}
82 local ps = luci.util.execi("df")
94 for value in line:gmatch("[^%s]+") do
101 -- this is a rather ugly workaround to cope with wrapped lines in
104 -- /dev/scsi/host0/bus0/target0/lun0/part3
105 -- 114382024 93566472 15005244 86% /mnt/usb
108 if not row[k[2]] then
111 for value in line:gmatch("[^%s]+") do
117 table.insert(data, row)
124 --- Retrieve environment variables. If no variable is given then a table
125 -- containing the whole environment is returned otherwise this function returns
126 -- the corresponding string value for the given name or nil if no such variable
130 -- @param var Name of the environment variable to retrieve (optional)
131 -- @return String containg the value of the specified variable
132 -- @return Table containing all variables if no variable name is given
133 getenv = nixio.getenv
135 --- Get or set the current hostname.
136 -- @param String containing a new hostname to set (optional)
137 -- @return String containing the system hostname
138 function hostname(newname)
139 if type(newname) == "string" and #newname > 0 then
140 fs.writefile( "/proc/sys/kernel/hostname", newname )
143 return nixio.uname().nodename
147 --- Returns the contents of a documented referred by an URL.
148 -- @param url The URL to retrieve
149 -- @param stream Return a stream instead of a buffer
150 -- @param target Directly write to target file name
151 -- @return String containing the contents of given the URL
152 function httpget(url, stream, target)
154 local source = stream and io.popen or luci.util.exec
155 return source("wget -qO- '"..url:gsub("'", "").."'")
157 return os.execute("wget -qO '%s' '%s'" %
158 {target:gsub("'", ""), url:gsub("'", "")})
162 --- Returns the system load average values.
163 -- @return String containing the average load value 1 minute ago
164 -- @return String containing the average load value 5 minutes ago
165 -- @return String containing the average load value 15 minutes ago
167 local info = nixio.sysinfo()
168 return info.loads[1], info.loads[2], info.loads[3]
171 --- Initiate a system reboot.
172 -- @return Return value of os.execute()
174 return os.execute("reboot >/dev/null 2>&1")
177 --- Returns the system type, cpu name and installed physical memory.
178 -- @return String containing the system or platform identifier
179 -- @return String containing hardware model information
180 -- @return String containing the total memory amount in kB
181 -- @return String containing the memory used for caching in kB
182 -- @return String containing the memory used for buffering in kB
183 -- @return String containing the free memory amount in kB
185 local cpuinfo = fs.readfile("/proc/cpuinfo")
186 local meminfo = fs.readfile("/proc/meminfo")
188 local system = cpuinfo:match("system typ.-:%s*([^\n]+)")
190 local memtotal = tonumber(meminfo:match("MemTotal:%s*(%d+)"))
191 local memcached = tonumber(meminfo:match("\nCached:%s*(%d+)"))
192 local memfree = tonumber(meminfo:match("MemFree:%s*(%d+)"))
193 local membuffers = tonumber(meminfo:match("Buffers:%s*(%d+)"))
196 system = nixio.uname().machine
197 model = cpuinfo:match("model name.-:%s*([^\n]+)")
199 model = cpuinfo:match("Processor.-:%s*([^\n]+)")
202 model = cpuinfo:match("cpu model.-:%s*([^\n]+)")
205 return system, model, memtotal, memcached, membuffers, memfree
208 --- Retrieves the output of the "logread" command.
209 -- @return String containing the current log buffer
211 return luci.util.exec("logread")
214 --- Retrieves the output of the "dmesg" command.
215 -- @return String containing the current log buffer
217 return luci.util.exec("dmesg")
220 --- Generates a random id with specified length.
221 -- @param bytes Number of bytes for the unique id
222 -- @return String containing hex encoded id
223 function uniqueid(bytes)
224 local rand = fs.readfile("/dev/urandom", bytes)
225 return rand and nixio.bin.hexlify(rand)
228 --- Returns the current system uptime stats.
229 -- @return String containing total uptime in seconds
231 return nixio.sysinfo().uptime
235 --- LuCI system utilities / network related functions.
237 -- @name luci.sys.net
240 --- Returns the current arp-table entries as two-dimensional table.
241 -- @return Table of table containing the current arp entries.
242 -- The following fields are defined for arp entry objects:
243 -- { "IP address", "HW address", "HW type", "Flags", "Mask", "Device" }
244 function net.arptable(callback)
245 return _parse_delimited_table(io.lines("/proc/net/arp"), "%s%s+", callback)
248 --- Returns conntrack information
249 -- @return Table with the currently tracked IP connections
250 function net.conntrack(callback)
252 if fs.access("/proc/net/nf_conntrack", "r") then
253 for line in io.lines("/proc/net/nf_conntrack") do
254 line = line:match "^(.-( [^ =]+=).-)%2"
255 local entry, flags = _parse_mixed_record(line, " +")
256 entry.layer3 = flags[1]
257 entry.layer4 = flags[3]
265 connt[#connt+1] = entry
268 elseif fs.access("/proc/net/ip_conntrack", "r") then
269 for line in io.lines("/proc/net/ip_conntrack") do
270 line = line:match "^(.-( [^ =]+=).-)%2"
271 local entry, flags = _parse_mixed_record(line, " +")
272 entry.layer3 = "ipv4"
273 entry.layer4 = flags[1]
281 connt[#connt+1] = entry
290 --- Determine the current IPv4 default route. If multiple default routes exist,
291 -- return the one with the lowest metric.
292 -- @return Table with the properties of the current default route.
293 -- The following fields are defined:
294 -- { "dest", "gateway", "metric", "refcount", "usecount", "irtt",
295 -- "flags", "device" }
296 function net.defaultroute()
299 net.routes(function(rt)
300 if rt.dest:prefix() == 0 and (not route or route.metric > rt.metric) then
308 --- Determine the current IPv6 default route. If multiple default routes exist,
309 -- return the one with the lowest metric.
310 -- @return Table with the properties of the current default route.
311 -- The following fields are defined:
312 -- { "source", "dest", "nexthop", "metric", "refcount", "usecount",
313 -- "flags", "device" }
314 function net.defaultroute6()
317 net.routes6(function(rt)
318 if rt.dest:prefix() == 0 and (not route or route.metric > rt.metric) then
326 --- Determine the names of available network interfaces.
327 -- @return Table containing all current interface names
328 function net.devices()
330 for k, v in ipairs(nixio.getifaddrs()) do
331 if v.family == "packet" then
332 devs[#devs+1] = v.name
339 --- Return information about available network interfaces.
340 -- @return Table containing all current interface names and their information
341 function net.deviceinfo()
343 for k, v in ipairs(nixio.getifaddrs()) do
344 if v.family == "packet" then
369 -- Determine the MAC address belonging to the given IP address.
370 -- @param ip IPv4 address
371 -- @return String containing the MAC address or nil if it cannot be found
372 function net.ip4mac(ip)
374 net.arptable(function(e)
375 if e["IP address"] == ip then
376 mac = e["HW address"]
382 --- Returns the current kernel routing table entries.
383 -- @return Table of tables with properties of the corresponding routes.
384 -- The following fields are defined for route entry tables:
385 -- { "dest", "gateway", "metric", "refcount", "usecount", "irtt",
386 -- "flags", "device" }
387 function net.routes(callback)
390 for line in io.lines("/proc/net/route") do
392 local dev, dst_ip, gateway, flags, refcnt, usecnt, metric,
393 dst_mask, mtu, win, irtt = line:match(
394 "([^%s]+)\t([A-F0-9]+)\t([A-F0-9]+)\t([A-F0-9]+)\t" ..
395 "(%d+)\t(%d+)\t(%d+)\t([A-F0-9]+)\t(%d+)\t(%d+)\t(%d+)"
399 gateway = luci.ip.Hex( gateway, 32, luci.ip.FAMILY_INET4 )
400 dst_mask = luci.ip.Hex( dst_mask, 32, luci.ip.FAMILY_INET4 )
401 dst_ip = luci.ip.Hex(
402 dst_ip, dst_mask:prefix(dst_mask), luci.ip.FAMILY_INET4
408 metric = tonumber(metric),
409 refcount = tonumber(refcnt),
410 usecount = tonumber(usecnt),
412 window = tonumber(window),
413 irtt = tonumber(irtt),
414 flags = tonumber(flags, 16),
421 routes[#routes+1] = rt
429 --- Returns the current ipv6 kernel routing table entries.
430 -- @return Table of tables with properties of the corresponding routes.
431 -- The following fields are defined for route entry tables:
432 -- { "source", "dest", "nexthop", "metric", "refcount", "usecount",
433 -- "flags", "device" }
434 function net.routes6(callback)
435 if fs.access("/proc/net/ipv6_route", "r") then
438 for line in io.lines("/proc/net/ipv6_route") do
440 local dst_ip, dst_prefix, src_ip, src_prefix, nexthop,
441 metric, refcnt, usecnt, flags, dev = line:match(
442 "([a-f0-9]+) ([a-f0-9]+) " ..
443 "([a-f0-9]+) ([a-f0-9]+) " ..
444 "([a-f0-9]+) ([a-f0-9]+) " ..
445 "([a-f0-9]+) ([a-f0-9]+) " ..
446 "([a-f0-9]+) +([^%s]+)"
449 src_ip = luci.ip.Hex(
450 src_ip, tonumber(src_prefix, 16), luci.ip.FAMILY_INET6, false
453 dst_ip = luci.ip.Hex(
454 dst_ip, tonumber(dst_prefix, 16), luci.ip.FAMILY_INET6, false
457 nexthop = luci.ip.Hex( nexthop, 128, luci.ip.FAMILY_INET6, false )
463 metric = tonumber(metric, 16),
464 refcount = tonumber(refcnt, 16),
465 usecount = tonumber(usecnt, 16),
466 flags = tonumber(flags, 16),
469 -- lua number is too small for storing the metric
470 -- add a metric_raw field with the original content
477 routes[#routes+1] = rt
485 --- Tests whether the given host responds to ping probes.
486 -- @param host String containing a hostname or IPv4 address
487 -- @return Number containing 0 on success and >= 1 on error
488 function net.pingtest(host)
489 return os.execute("ping -c1 '"..host:gsub("'", '').."' >/dev/null 2>&1")
493 --- LuCI system utilities / process related functions.
495 -- @name luci.sys.process
498 --- Get the current process id.
500 -- @name process.info
501 -- @return Number containing the current pid
502 function process.info(key)
503 local s = {uid = nixio.getuid(), gid = nixio.getgid()}
504 return not key and s or s[key]
507 --- Retrieve information about currently running processes.
508 -- @return Table containing process information
509 function process.list()
512 local ps = luci.util.execi("top -bn1")
524 k = luci.util.split(luci.util.trim(line), "%s+", nil, true)
525 if k[1] == "PID" then
533 line = luci.util.trim(line)
534 for i, value in ipairs(luci.util.split(line, "%s+", #k-1, true)) do
538 local pid = tonumber(row[k[1]])
547 --- Set the gid of a process identified by given pid.
548 -- @param gid Number containing the Unix group id
549 -- @return Boolean indicating successful operation
550 -- @return String containing the error message if failed
551 -- @return Number containing the error code if failed
552 function process.setgroup(gid)
553 return nixio.setgid(gid)
556 --- Set the uid of a process identified by given pid.
557 -- @param uid Number containing the Unix user id
558 -- @return Boolean indicating successful operation
559 -- @return String containing the error message if failed
560 -- @return Number containing the error code if failed
561 function process.setuser(uid)
562 return nixio.setuid(uid)
565 --- Send a signal to a process identified by given pid.
567 -- @name process.signal
568 -- @param pid Number containing the process id
569 -- @param sig Signal to send (default: 15 [SIGTERM])
570 -- @return Boolean indicating successful operation
571 -- @return Number containing the error code if failed
572 process.signal = nixio.kill
575 --- LuCI system utilities / user related functions.
577 -- @name luci.sys.user
580 --- Retrieve user informations for given uid.
583 -- @param uid Number containing the Unix user id
584 -- @return Table containing the following fields:
585 -- { "uid", "gid", "name", "passwd", "dir", "shell", "gecos" }
586 user.getuser = nixio.getpw
588 --- Retrieve the current user password hash.
589 -- @param username String containing the username to retrieve the password for
590 -- @return String containing the hash or nil if no password is set.
591 function user.getpasswd(username)
592 local pwe = nixio.getsp and nixio.getsp(username) or nixio.getpw(username)
593 local pwh = pwe and (pwe.pwdp or pwe.passwd)
594 if not pwh or #pwh < 1 or pwh == "!" or pwh == "x" then
601 --- Test whether given string matches the password of a given system user.
602 -- @param username String containing the Unix user name
603 -- @param pass String containing the password to compare
604 -- @return Boolean indicating wheather the passwords are equal
605 function user.checkpasswd(username, pass)
606 local pwh = user.getpasswd(username)
607 if not pwh or nixio.crypt(pass, pwh) ~= pwh then
614 --- Change the password of given user.
615 -- @param username String containing the Unix user name
616 -- @param password String containing the password to compare
617 -- @return Number containing 0 on success and >= 1 on error
618 function user.setpasswd(username, password)
620 password = password:gsub("'", "")
624 username = username:gsub("'", "")
627 local cmd = "(echo '"..password.."';sleep 1;echo '"..password.."')|"
628 cmd = cmd .. "passwd '"..username.."' >/dev/null 2>&1"
629 return os.execute(cmd)
633 --- LuCI system utilities / wifi related functions.
635 -- @name luci.sys.wifi
638 --- Get wireless information for given interface.
639 -- @param ifname String containing the interface name
640 -- @return A wrapped iwinfo object instance
641 function wifi.getiwinfo(ifname)
644 local u = uci.cursor_state()
645 local d, n = ifname:match("^(%w+)%.network(%d+)")
648 u:foreach("wireless", "wifi-iface",
650 if s.device == d then
653 ifname = s.ifname or s.device
658 elseif u:get("wireless", ifname) == "wifi-device" then
659 u:foreach("wireless", "wifi-iface",
661 if s.device == ifname and s.ifname then
668 local t = iwinfo.type(ifname)
671 return setmetatable({}, {
672 __index = function(t, k)
673 if k == "ifname" then
684 --- Get iwconfig output for all wireless devices.
685 -- @return Table of tables containing the iwconfing output for each wifi device
686 function wifi.getiwconfig()
687 local cnt = luci.util.exec("PATH=/sbin:/usr/sbin iwconfig 2>/dev/null")
690 for i, l in pairs(luci.util.split(luci.util.trim(cnt), "\n\n")) do
691 local k = l:match("^(.-) ")
692 l = l:gsub("^(.-) +", "", 1)
694 local entry, flags = _parse_mixed_record(l)
705 --- Get iwlist scan output from all wireless devices.
706 -- @return Table of tables contaiing all scan results
707 function wifi.iwscan(iface)
708 local siface = iface or ""
709 local cnt = luci.util.exec("iwlist "..siface.." scan 2>/dev/null")
712 for i, l in pairs(luci.util.split(luci.util.trim(cnt), "\n\n")) do
713 local k = l:match("^(.-) ")
714 l = l:gsub("^[^\n]+", "", 1)
715 l = luci.util.trim(l)
718 for j, c in pairs(luci.util.split(l, "\n Cell")) do
719 c = c:gsub("^(.-)- ", "", 1)
720 c = luci.util.split(c, "\n", 7)
721 c = table.concat(c, "\n", 1)
722 local entry, flags = _parse_mixed_record(c)
726 table.insert(iws[k], entry)
731 return iface and (iws[iface] or {}) or iws
734 --- Get available channels from given wireless iface.
735 -- @param iface Wireless interface (optional)
736 -- @return Table of available channels
737 function wifi.channels(iface)
738 local t = iwinfo.type(iface or "")
740 if iface and t and iwinfo[t] then
741 cns = iwinfo[t].freqlist(iface)
744 if not cns or #cns == 0 then
746 {channel = 1, mhz = 2412},
747 {channel = 2, mhz = 2417},
748 {channel = 3, mhz = 2422},
749 {channel = 4, mhz = 2427},
750 {channel = 5, mhz = 2432},
751 {channel = 6, mhz = 2437},
752 {channel = 7, mhz = 2442},
753 {channel = 8, mhz = 2447},
754 {channel = 9, mhz = 2452},
755 {channel = 10, mhz = 2457},
756 {channel = 11, mhz = 2462}
764 --- LuCI system utilities / init related functions.
766 -- @name luci.sys.init
768 init.dir = "/etc/init.d/"
770 --- Get the names of all installed init scripts
771 -- @return Table containing the names of all inistalled init scripts
772 function init.names()
774 for name in fs.glob(init.dir.."*") do
775 names[#names+1] = fs.basename(name)
780 --- Test whether the given init script is enabled
781 -- @param name Name of the init script
782 -- @return Boolean indicating whether init is enabled
783 function init.enabled(name)
784 if fs.access(init.dir..name) then
785 return ( call(init.dir..name.." enabled") == 0 )
790 --- Get the index of he given init script
791 -- @param name Name of the init script
792 -- @return Numeric index value
793 function init.index(name)
794 if fs.access(init.dir..name) then
795 return call("source "..init.dir..name.." enabled; exit $START")
799 --- Enable the given init script
800 -- @param name Name of the init script
801 -- @return Boolean indicating success
802 function init.enable(name)
803 if fs.access(init.dir..name) then
804 return ( call(init.dir..name.." enable") == 1 )
808 --- Disable the given init script
809 -- @param name Name of the init script
810 -- @return Boolean indicating success
811 function init.disable(name)
812 if fs.access(init.dir..name) then
813 return ( call(init.dir..name.." disable") == 0 )
818 -- Internal functions
820 function _parse_delimited_table(iter, delimiter, callback)
821 delimiter = delimiter or "%s+"
824 local trim = luci.util.trim
825 local split = luci.util.split
827 local keys = split(trim(iter()), delimiter, nil, true)
828 for i, j in pairs(keys) do
829 keys[i] = trim(keys[i])
836 for i, j in pairs(split(line, delimiter, nil, true)) do
853 function _parse_mixed_record(cnt, delimiter)
854 delimiter = delimiter or " "
858 for i, l in pairs(luci.util.split(luci.util.trim(cnt), "\n")) do
859 for j, f in pairs(luci.util.split(luci.util.trim(l), delimiter, nil, true)) do
860 local k, x, v = f:match('([^%s][^:=]*) *([:=]*) *"*([^\n"]*)"*')
864 table.insert(flags, k)