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"
35 luci.util = require "luci.util"
36 luci.ip = require "luci.ip"
38 local tonumber, ipairs, pairs, pcall, type, next =
39 tonumber, ipairs, pairs, pcall, type, next
42 --- LuCI Linux and POSIX system utilities.
45 --- Execute a given shell command and return the error code
48 -- @param ... Command to call
49 -- @return Error code of the command
51 return os.execute(...) / 256
54 --- Execute a given shell command and capture its standard output
57 -- @param command Command to call
58 -- @return String containg the return the output of the command
61 --- Invoke the luci-flash executable to write an image to the flash memory.
62 -- @param image Local path or URL to image file
63 -- @param kpattern Pattern of files to keep over flash process
64 -- @return Return value of os.execute()
65 function flash(image, kpattern)
66 local cmd = "luci-flash "
68 cmd = cmd .. "-k '" .. kpattern:gsub("'", "") .. "' "
70 cmd = cmd .. "'" .. image:gsub("'", "") .. "' >/dev/null 2>&1"
72 return os.execute(cmd)
75 --- Retrieve information about currently mounted file systems.
76 -- @return Table containing mount information
79 local k = {"fs", "blocks", "used", "available", "percent", "mountpoint"}
80 local ps = luci.util.execi("df")
92 for value in line:gmatch("[^%s]+") do
99 -- this is a rather ugly workaround to cope with wrapped lines in
102 -- /dev/scsi/host0/bus0/target0/lun0/part3
103 -- 114382024 93566472 15005244 86% /mnt/usb
106 if not row[k[2]] then
109 for value in line:gmatch("[^%s]+") do
115 table.insert(data, row)
122 --- Retrieve environment variables. If no variable is given then a table
123 -- containing the whole environment is returned otherwise this function returns
124 -- the corresponding string value for the given name or nil if no such variable
128 -- @param var Name of the environment variable to retrieve (optional)
129 -- @return String containg the value of the specified variable
130 -- @return Table containing all variables if no variable name is given
131 getenv = nixio.getenv
133 --- Get or set the current hostname.
134 -- @param String containing a new hostname to set (optional)
135 -- @return String containing the system hostname
136 function hostname(newname)
137 if type(newname) == "string" and #newname > 0 then
138 fs.writefile( "/proc/sys/kernel/hostname", newname )
141 return nixio.uname().nodename
145 --- Returns the contents of a documented referred by an URL.
146 -- @param url The URL to retrieve
147 -- @param stream Return a stream instead of a buffer
148 -- @param target Directly write to target file name
149 -- @return String containing the contents of given the URL
150 function httpget(url, stream, target)
152 local source = stream and io.popen or luci.util.exec
153 return source("wget -qO- '"..url:gsub("'", "").."'")
155 return os.execute("wget -qO '%s' '%s'" %
156 {target:gsub("'", ""), url:gsub("'", "")})
160 --- Returns the system load average values.
161 -- @return String containing the average load value 1 minute ago
162 -- @return String containing the average load value 5 minutes ago
163 -- @return String containing the average load value 15 minutes ago
165 local info = nixio.sysinfo()
166 return info.loads[1], info.loads[2], info.loads[3]
169 --- Initiate a system reboot.
170 -- @return Return value of os.execute()
172 return os.execute("reboot >/dev/null 2>&1")
175 --- Returns the system type, cpu name and installed physical memory.
176 -- @return String containing the system or platform identifier
177 -- @return String containing hardware model information
178 -- @return String containing the total memory amount in kB
179 -- @return String containing the memory used for caching in kB
180 -- @return String containing the memory used for buffering in kB
181 -- @return String containing the free memory amount in kB
183 local cpuinfo = fs.readfile("/proc/cpuinfo")
184 local meminfo = fs.readfile("/proc/meminfo")
186 local system = cpuinfo:match("system typ.-:%s*([^\n]+)")
188 local memtotal = tonumber(meminfo:match("MemTotal:%s*(%d+)"))
189 local memcached = tonumber(meminfo:match("\nCached:%s*(%d+)"))
190 local memfree = tonumber(meminfo:match("MemFree:%s*(%d+)"))
191 local membuffers = tonumber(meminfo:match("Buffers:%s*(%d+)"))
194 system = nixio.uname().machine
195 model = cpuinfo:match("model name.-:%s*([^\n]+)")
197 model = cpuinfo:match("Processor.-:%s*([^\n]+)")
200 model = cpuinfo:match("cpu model.-:%s*([^\n]+)")
203 return system, model, memtotal, memcached, membuffers, memfree
206 --- Retrieves the output of the "logread" command.
207 -- @return String containing the current log buffer
209 return luci.util.exec("logread")
212 --- Retrieves the output of the "dmesg" command.
213 -- @return String containing the current log buffer
215 return luci.util.exec("dmesg")
218 --- Generates a random id with specified length.
219 -- @param bytes Number of bytes for the unique id
220 -- @return String containing hex encoded id
221 function uniqueid(bytes)
222 local rand = fs.readfile("/dev/urandom", bytes)
223 return rand and nixio.bin.hexlify(rand)
226 --- Returns the current system uptime stats.
227 -- @return String containing total uptime in seconds
229 return nixio.sysinfo().uptime
233 --- LuCI system utilities / network related functions.
235 -- @name luci.sys.net
238 --- Returns the current arp-table entries as two-dimensional table.
239 -- @return Table of table containing the current arp entries.
240 -- The following fields are defined for arp entry objects:
241 -- { "IP address", "HW address", "HW type", "Flags", "Mask", "Device" }
242 function net.arptable(callback)
243 return _parse_delimited_table(io.lines("/proc/net/arp"), "%s%s+", callback)
246 --- Returns conntrack information
247 -- @return Table with the currently tracked IP connections
248 function net.conntrack(callback)
250 if fs.access("/proc/net/nf_conntrack", "r") then
251 for line in io.lines("/proc/net/nf_conntrack") do
252 line = line:match "^(.-( [^ =]+=).-)%2"
253 local entry, flags = _parse_mixed_record(line, " +")
254 entry.layer3 = flags[1]
255 entry.layer4 = flags[3]
263 connt[#connt+1] = entry
266 elseif fs.access("/proc/net/ip_conntrack", "r") then
267 for line in io.lines("/proc/net/ip_conntrack") do
268 line = line:match "^(.-( [^ =]+=).-)%2"
269 local entry, flags = _parse_mixed_record(line, " +")
270 entry.layer3 = "ipv4"
271 entry.layer4 = flags[1]
279 connt[#connt+1] = entry
288 --- Determine the current IPv4 default route. If multiple default routes exist,
289 -- return the one with the lowest metric.
290 -- @return Table with the properties of the current default route.
291 -- The following fields are defined:
292 -- { "dest", "gateway", "metric", "refcount", "usecount", "irtt",
293 -- "flags", "device" }
294 function net.defaultroute()
297 net.routes(function(rt)
298 if rt.dest:prefix() == 0 and (not route or route.metric > rt.metric) then
306 --- Determine the current IPv6 default route. If multiple default routes exist,
307 -- return the one with the lowest metric.
308 -- @return Table with the properties of the current default route.
309 -- The following fields are defined:
310 -- { "source", "dest", "nexthop", "metric", "refcount", "usecount",
311 -- "flags", "device" }
312 function net.defaultroute6()
315 net.routes6(function(rt)
316 if rt.dest:prefix() == 0 and (not route or route.metric > rt.metric) then
324 --- Determine the names of available network interfaces.
325 -- @return Table containing all current interface names
326 function net.devices()
328 for k, v in ipairs(nixio.getifaddrs()) do
329 if v.family == "packet" then
330 devs[#devs+1] = v.name
337 --- Return information about available network interfaces.
338 -- @return Table containing all current interface names and their information
339 function net.deviceinfo()
341 for k, v in ipairs(nixio.getifaddrs()) do
342 if v.family == "packet" then
367 -- Determine the MAC address belonging to the given IP address.
368 -- @param ip IPv4 address
369 -- @return String containing the MAC address or nil if it cannot be found
370 function net.ip4mac(ip)
372 net.arptable(function(e)
373 if e["IP address"] == ip then
374 mac = e["HW address"]
380 --- Returns the current kernel routing table entries.
381 -- @return Table of tables with properties of the corresponding routes.
382 -- The following fields are defined for route entry tables:
383 -- { "dest", "gateway", "metric", "refcount", "usecount", "irtt",
384 -- "flags", "device" }
385 function net.routes(callback)
388 for line in io.lines("/proc/net/route") do
390 local dev, dst_ip, gateway, flags, refcnt, usecnt, metric,
391 dst_mask, mtu, win, irtt = line:match(
392 "([^%s]+)\t([A-F0-9]+)\t([A-F0-9]+)\t([A-F0-9]+)\t" ..
393 "(%d+)\t(%d+)\t(%d+)\t([A-F0-9]+)\t(%d+)\t(%d+)\t(%d+)"
397 gateway = luci.ip.Hex( gateway, 32, luci.ip.FAMILY_INET4 )
398 dst_mask = luci.ip.Hex( dst_mask, 32, luci.ip.FAMILY_INET4 )
399 dst_ip = luci.ip.Hex(
400 dst_ip, dst_mask:prefix(dst_mask), luci.ip.FAMILY_INET4
406 metric = tonumber(metric),
407 refcount = tonumber(refcnt),
408 usecount = tonumber(usecnt),
410 window = tonumber(window),
411 irtt = tonumber(irtt),
412 flags = tonumber(flags, 16),
419 routes[#routes+1] = rt
427 --- Returns the current ipv6 kernel routing table entries.
428 -- @return Table of tables with properties of the corresponding routes.
429 -- The following fields are defined for route entry tables:
430 -- { "source", "dest", "nexthop", "metric", "refcount", "usecount",
431 -- "flags", "device" }
432 function net.routes6(callback)
433 if fs.access("/proc/net/ipv6_route", "r") then
436 for line in io.lines("/proc/net/ipv6_route") do
438 local dst_ip, dst_prefix, src_ip, src_prefix, nexthop,
439 metric, refcnt, usecnt, flags, dev = line:match(
440 "([a-f0-9]+) ([a-f0-9]+) " ..
441 "([a-f0-9]+) ([a-f0-9]+) " ..
442 "([a-f0-9]+) ([a-f0-9]+) " ..
443 "([a-f0-9]+) ([a-f0-9]+) " ..
444 "([a-f0-9]+) +([^%s]+)"
447 src_ip = luci.ip.Hex(
448 src_ip, tonumber(src_prefix, 16), luci.ip.FAMILY_INET6, false
451 dst_ip = luci.ip.Hex(
452 dst_ip, tonumber(dst_prefix, 16), luci.ip.FAMILY_INET6, false
455 nexthop = luci.ip.Hex( nexthop, 128, luci.ip.FAMILY_INET6, false )
461 metric = tonumber(metric, 16),
462 refcount = tonumber(refcnt, 16),
463 usecount = tonumber(usecnt, 16),
464 flags = tonumber(flags, 16),
471 routes[#routes+1] = rt
479 --- Tests whether the given host responds to ping probes.
480 -- @param host String containing a hostname or IPv4 address
481 -- @return Number containing 0 on success and >= 1 on error
482 function net.pingtest(host)
483 return os.execute("ping -c1 '"..host:gsub("'", '').."' >/dev/null 2>&1")
487 --- LuCI system utilities / process related functions.
489 -- @name luci.sys.process
492 --- Get the current process id.
494 -- @name process.info
495 -- @return Number containing the current pid
496 function process.info(key)
497 local s = {uid = nixio.getuid(), gid = nixio.getgid()}
498 return not key and s or s[key]
501 --- Retrieve information about currently running processes.
502 -- @return Table containing process information
503 function process.list()
506 local ps = luci.util.execi("top -bn1")
518 k = luci.util.split(luci.util.trim(line), "%s+", nil, true)
519 if k[1] == "PID" then
527 line = luci.util.trim(line)
528 for i, value in ipairs(luci.util.split(line, "%s+", #k-1, true)) do
532 local pid = tonumber(row[k[1]])
541 --- Set the gid of a process identified by given pid.
542 -- @param gid Number containing the Unix group 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.setgroup(gid)
547 return nixio.setgid(gid)
550 --- Set the uid of a process identified by given pid.
551 -- @param uid Number containing the Unix user id
552 -- @return Boolean indicating successful operation
553 -- @return String containing the error message if failed
554 -- @return Number containing the error code if failed
555 function process.setuser(uid)
556 return nixio.setuid(uid)
559 --- Send a signal to a process identified by given pid.
561 -- @name process.signal
562 -- @param pid Number containing the process id
563 -- @param sig Signal to send (default: 15 [SIGTERM])
564 -- @return Boolean indicating successful operation
565 -- @return Number containing the error code if failed
566 process.signal = nixio.kill
569 --- LuCI system utilities / user related functions.
571 -- @name luci.sys.user
574 --- Retrieve user informations for given uid.
577 -- @param uid Number containing the Unix user id
578 -- @return Table containing the following fields:
579 -- { "uid", "gid", "name", "passwd", "dir", "shell", "gecos" }
580 user.getuser = nixio.getpw
582 --- Test whether given string matches the password of a given system user.
583 -- @param username String containing the Unix user name
584 -- @param pass String containing the password to compare
585 -- @return Boolean indicating wheather the passwords are equal
586 function user.checkpasswd(username, pass)
587 local pwe = nixio.getsp and nixio.getsp(username) or nixio.getpw(username)
588 local pwh = pwe and (pwe.pwdp or pwe.passwd)
589 if not pwh or #pwh < 1 or pwh ~= "!" and nixio.crypt(pass, pwh) ~= pwh then
596 --- Change the password of given user.
597 -- @param username String containing the Unix user name
598 -- @param password String containing the password to compare
599 -- @return Number containing 0 on success and >= 1 on error
600 function user.setpasswd(username, password)
602 password = password:gsub("'", "")
606 username = username:gsub("'", "")
609 local cmd = "(echo '"..password.."';sleep 1;echo '"..password.."')|"
610 cmd = cmd .. "passwd '"..username.."' >/dev/null 2>&1"
611 return os.execute(cmd)
615 --- LuCI system utilities / wifi related functions.
617 -- @name luci.sys.wifi
620 --- Get iwconfig output for all wireless devices.
621 -- @return Table of tables containing the iwconfing output for each wifi device
622 function wifi.getiwconfig()
623 local cnt = luci.util.exec("PATH=/sbin:/usr/sbin iwconfig 2>/dev/null")
626 for i, l in pairs(luci.util.split(luci.util.trim(cnt), "\n\n")) do
627 local k = l:match("^(.-) ")
628 l = l:gsub("^(.-) +", "", 1)
630 local entry, flags = _parse_mixed_record(l)
641 --- Get iwlist scan output from all wireless devices.
642 -- @return Table of tables contaiing all scan results
643 function wifi.iwscan(iface)
644 local siface = iface or ""
645 local cnt = luci.util.exec("iwlist "..siface.." scan 2>/dev/null")
648 for i, l in pairs(luci.util.split(luci.util.trim(cnt), "\n\n")) do
649 local k = l:match("^(.-) ")
650 l = l:gsub("^[^\n]+", "", 1)
651 l = luci.util.trim(l)
654 for j, c in pairs(luci.util.split(l, "\n Cell")) do
655 c = c:gsub("^(.-)- ", "", 1)
656 c = luci.util.split(c, "\n", 7)
657 c = table.concat(c, "\n", 1)
658 local entry, flags = _parse_mixed_record(c)
662 table.insert(iws[k], entry)
667 return iface and (iws[iface] or {}) or iws
670 --- Get available channels from given wireless iface.
671 -- @param iface Wireless interface (optional)
672 -- @return Table of available channels
673 function wifi.channels(iface)
674 local cmd = "iwlist " .. ( iface or "" ) .. " freq 2>/dev/null"
677 local fd = io.popen(cmd)
682 if not ln then break end
683 c, f = ln:match("Channel (%d+) : (%d+%.%d+) GHz")
685 cns[tonumber(c)] = tonumber(f)
691 if not next(cns) then
693 2.412, 2.417, 2.422, 2.427, 2.432, 2.437,
694 2.442, 2.447, 2.452, 2.457, 2.462
702 --- LuCI system utilities / init related functions.
704 -- @name luci.sys.init
706 init.dir = "/etc/init.d/"
708 --- Get the names of all installed init scripts
709 -- @return Table containing the names of all inistalled init scripts
710 function init.names()
712 for name in fs.glob(init.dir.."*") do
713 names[#names+1] = fs.basename(name)
718 --- Test whether the given init script is enabled
719 -- @param name Name of the init script
720 -- @return Boolean indicating whether init is enabled
721 function init.enabled(name)
722 if fs.access(init.dir..name) then
723 return ( call(init.dir..name.." enabled") == 0 )
728 --- Get the index of he given init script
729 -- @param name Name of the init script
730 -- @return Numeric index value
731 function init.index(name)
732 if fs.access(init.dir..name) then
733 return call("source "..init.dir..name.."; exit $START")
737 --- Enable the given init script
738 -- @param name Name of the init script
739 -- @return Boolean indicating success
740 function init.enable(name)
741 if fs.access(init.dir..name) then
742 return ( call(init.dir..name.." enable") == 1 )
746 --- Disable the given init script
747 -- @param name Name of the init script
748 -- @return Boolean indicating success
749 function init.disable(name)
750 if fs.access(init.dir..name) then
751 return ( call(init.dir..name.." disable") == 0 )
756 -- Internal functions
758 function _parse_delimited_table(iter, delimiter, callback)
759 delimiter = delimiter or "%s+"
762 local trim = luci.util.trim
763 local split = luci.util.split
765 local keys = split(trim(iter()), delimiter, nil, true)
766 for i, j in pairs(keys) do
767 keys[i] = trim(keys[i])
774 for i, j in pairs(split(line, delimiter, nil, true)) do
791 function _parse_mixed_record(cnt, delimiter)
792 delimiter = delimiter or " "
796 for i, l in pairs(luci.util.split(luci.util.trim(cnt), "\n")) do
797 for j, f in pairs(luci.util.split(luci.util.trim(l), delimiter, nil, true)) do
798 local k, x, v = f:match('([^%s][^:=]*) *([:=]*) *"*([^\n"]*)"*')
802 table.insert(flags, k)