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"
36 luci.util = require "luci.util"
37 luci.ip = require "luci.ip"
39 local tonumber, ipairs, pairs, pcall, type, next, setmetatable =
40 tonumber, ipairs, pairs, pcall, type, next, setmetatable
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 --- Invoke the luci-flash executable to write an image to the flash memory.
63 -- @param image Local path or URL to image file
64 -- @param kpattern Pattern of files to keep over flash process
65 -- @return Return value of os.execute()
66 function flash(image, kpattern)
67 local cmd = "luci-flash "
69 cmd = cmd .. "-k '" .. kpattern:gsub("'", "") .. "' "
71 cmd = cmd .. "'" .. image:gsub("'", "") .. "' >/dev/null 2>&1"
73 return os.execute(cmd)
76 --- Retrieve information about currently mounted file systems.
77 -- @return Table containing mount information
80 local k = {"fs", "blocks", "used", "available", "percent", "mountpoint"}
81 local ps = luci.util.execi("df")
93 for value in line:gmatch("[^%s]+") do
100 -- this is a rather ugly workaround to cope with wrapped lines in
103 -- /dev/scsi/host0/bus0/target0/lun0/part3
104 -- 114382024 93566472 15005244 86% /mnt/usb
107 if not row[k[2]] then
110 for value in line:gmatch("[^%s]+") do
116 table.insert(data, row)
123 --- Retrieve environment variables. If no variable is given then a table
124 -- containing the whole environment is returned otherwise this function returns
125 -- the corresponding string value for the given name or nil if no such variable
129 -- @param var Name of the environment variable to retrieve (optional)
130 -- @return String containg the value of the specified variable
131 -- @return Table containing all variables if no variable name is given
132 getenv = nixio.getenv
134 --- Get or set the current hostname.
135 -- @param String containing a new hostname to set (optional)
136 -- @return String containing the system hostname
137 function hostname(newname)
138 if type(newname) == "string" and #newname > 0 then
139 fs.writefile( "/proc/sys/kernel/hostname", newname )
142 return nixio.uname().nodename
146 --- Returns the contents of a documented referred by an URL.
147 -- @param url The URL to retrieve
148 -- @param stream Return a stream instead of a buffer
149 -- @param target Directly write to target file name
150 -- @return String containing the contents of given the URL
151 function httpget(url, stream, target)
153 local source = stream and io.popen or luci.util.exec
154 return source("wget -qO- '"..url:gsub("'", "").."'")
156 return os.execute("wget -qO '%s' '%s'" %
157 {target:gsub("'", ""), url:gsub("'", "")})
161 --- Returns the system load average values.
162 -- @return String containing the average load value 1 minute ago
163 -- @return String containing the average load value 5 minutes ago
164 -- @return String containing the average load value 15 minutes ago
166 local info = nixio.sysinfo()
167 return info.loads[1], info.loads[2], info.loads[3]
170 --- Initiate a system reboot.
171 -- @return Return value of os.execute()
173 return os.execute("reboot >/dev/null 2>&1")
176 --- Returns the system type, cpu name and installed physical memory.
177 -- @return String containing the system or platform identifier
178 -- @return String containing hardware model information
179 -- @return String containing the total memory amount in kB
180 -- @return String containing the memory used for caching in kB
181 -- @return String containing the memory used for buffering in kB
182 -- @return String containing the free memory amount in kB
184 local cpuinfo = fs.readfile("/proc/cpuinfo")
185 local meminfo = fs.readfile("/proc/meminfo")
187 local system = cpuinfo:match("system typ.-:%s*([^\n]+)")
189 local memtotal = tonumber(meminfo:match("MemTotal:%s*(%d+)"))
190 local memcached = tonumber(meminfo:match("\nCached:%s*(%d+)"))
191 local memfree = tonumber(meminfo:match("MemFree:%s*(%d+)"))
192 local membuffers = tonumber(meminfo:match("Buffers:%s*(%d+)"))
195 system = nixio.uname().machine
196 model = cpuinfo:match("model name.-:%s*([^\n]+)")
198 model = cpuinfo:match("Processor.-:%s*([^\n]+)")
201 model = cpuinfo:match("cpu model.-:%s*([^\n]+)")
204 return system, model, memtotal, memcached, membuffers, memfree
207 --- Retrieves the output of the "logread" command.
208 -- @return String containing the current log buffer
210 return luci.util.exec("logread")
213 --- Retrieves the output of the "dmesg" command.
214 -- @return String containing the current log buffer
216 return luci.util.exec("dmesg")
219 --- Generates a random id with specified length.
220 -- @param bytes Number of bytes for the unique id
221 -- @return String containing hex encoded id
222 function uniqueid(bytes)
223 local rand = fs.readfile("/dev/urandom", bytes)
224 return rand and nixio.bin.hexlify(rand)
227 --- Returns the current system uptime stats.
228 -- @return String containing total uptime in seconds
230 return nixio.sysinfo().uptime
234 --- LuCI system utilities / network related functions.
236 -- @name luci.sys.net
239 --- Returns the current arp-table entries as two-dimensional table.
240 -- @return Table of table containing the current arp entries.
241 -- The following fields are defined for arp entry objects:
242 -- { "IP address", "HW address", "HW type", "Flags", "Mask", "Device" }
243 function net.arptable(callback)
244 return _parse_delimited_table(io.lines("/proc/net/arp"), "%s%s+", callback)
247 --- Returns conntrack information
248 -- @return Table with the currently tracked IP connections
249 function net.conntrack(callback)
251 if fs.access("/proc/net/nf_conntrack", "r") then
252 for line in io.lines("/proc/net/nf_conntrack") do
253 line = line:match "^(.-( [^ =]+=).-)%2"
254 local entry, flags = _parse_mixed_record(line, " +")
255 entry.layer3 = flags[1]
256 entry.layer4 = flags[3]
264 connt[#connt+1] = entry
267 elseif fs.access("/proc/net/ip_conntrack", "r") then
268 for line in io.lines("/proc/net/ip_conntrack") do
269 line = line:match "^(.-( [^ =]+=).-)%2"
270 local entry, flags = _parse_mixed_record(line, " +")
271 entry.layer3 = "ipv4"
272 entry.layer4 = flags[1]
280 connt[#connt+1] = entry
289 --- Determine the current IPv4 default route. If multiple default routes exist,
290 -- return the one with the lowest metric.
291 -- @return Table with the properties of the current default route.
292 -- The following fields are defined:
293 -- { "dest", "gateway", "metric", "refcount", "usecount", "irtt",
294 -- "flags", "device" }
295 function net.defaultroute()
298 net.routes(function(rt)
299 if rt.dest:prefix() == 0 and (not route or route.metric > rt.metric) then
307 --- Determine the current IPv6 default route. If multiple default routes exist,
308 -- return the one with the lowest metric.
309 -- @return Table with the properties of the current default route.
310 -- The following fields are defined:
311 -- { "source", "dest", "nexthop", "metric", "refcount", "usecount",
312 -- "flags", "device" }
313 function net.defaultroute6()
316 net.routes6(function(rt)
317 if rt.dest:prefix() == 0 and (not route or route.metric > rt.metric) then
325 --- Determine the names of available network interfaces.
326 -- @return Table containing all current interface names
327 function net.devices()
329 for k, v in ipairs(nixio.getifaddrs()) do
330 if v.family == "packet" then
331 devs[#devs+1] = v.name
338 --- Return information about available network interfaces.
339 -- @return Table containing all current interface names and their information
340 function net.deviceinfo()
342 for k, v in ipairs(nixio.getifaddrs()) do
343 if v.family == "packet" then
368 -- Determine the MAC address belonging to the given IP address.
369 -- @param ip IPv4 address
370 -- @return String containing the MAC address or nil if it cannot be found
371 function net.ip4mac(ip)
373 net.arptable(function(e)
374 if e["IP address"] == ip then
375 mac = e["HW address"]
381 --- Returns the current kernel routing table entries.
382 -- @return Table of tables with properties of the corresponding routes.
383 -- The following fields are defined for route entry tables:
384 -- { "dest", "gateway", "metric", "refcount", "usecount", "irtt",
385 -- "flags", "device" }
386 function net.routes(callback)
389 for line in io.lines("/proc/net/route") do
391 local dev, dst_ip, gateway, flags, refcnt, usecnt, metric,
392 dst_mask, mtu, win, irtt = line:match(
393 "([^%s]+)\t([A-F0-9]+)\t([A-F0-9]+)\t([A-F0-9]+)\t" ..
394 "(%d+)\t(%d+)\t(%d+)\t([A-F0-9]+)\t(%d+)\t(%d+)\t(%d+)"
398 gateway = luci.ip.Hex( gateway, 32, luci.ip.FAMILY_INET4 )
399 dst_mask = luci.ip.Hex( dst_mask, 32, luci.ip.FAMILY_INET4 )
400 dst_ip = luci.ip.Hex(
401 dst_ip, dst_mask:prefix(dst_mask), luci.ip.FAMILY_INET4
407 metric = tonumber(metric),
408 refcount = tonumber(refcnt),
409 usecount = tonumber(usecnt),
411 window = tonumber(window),
412 irtt = tonumber(irtt),
413 flags = tonumber(flags, 16),
420 routes[#routes+1] = rt
428 --- Returns the current ipv6 kernel routing table entries.
429 -- @return Table of tables with properties of the corresponding routes.
430 -- The following fields are defined for route entry tables:
431 -- { "source", "dest", "nexthop", "metric", "refcount", "usecount",
432 -- "flags", "device" }
433 function net.routes6(callback)
434 if fs.access("/proc/net/ipv6_route", "r") then
437 for line in io.lines("/proc/net/ipv6_route") do
439 local dst_ip, dst_prefix, src_ip, src_prefix, nexthop,
440 metric, refcnt, usecnt, flags, dev = line:match(
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]+) ([a-f0-9]+) " ..
445 "([a-f0-9]+) +([^%s]+)"
448 src_ip = luci.ip.Hex(
449 src_ip, tonumber(src_prefix, 16), luci.ip.FAMILY_INET6, false
452 dst_ip = luci.ip.Hex(
453 dst_ip, tonumber(dst_prefix, 16), luci.ip.FAMILY_INET6, false
456 nexthop = luci.ip.Hex( nexthop, 128, luci.ip.FAMILY_INET6, false )
462 metric = tonumber(metric, 16),
463 refcount = tonumber(refcnt, 16),
464 usecount = tonumber(usecnt, 16),
465 flags = tonumber(flags, 16),
468 -- lua number is too small for storing the metric
469 -- add a metric_raw field with the original content
476 routes[#routes+1] = rt
484 --- Tests whether the given host responds to ping probes.
485 -- @param host String containing a hostname or IPv4 address
486 -- @return Number containing 0 on success and >= 1 on error
487 function net.pingtest(host)
488 return os.execute("ping -c1 '"..host:gsub("'", '').."' >/dev/null 2>&1")
492 --- LuCI system utilities / process related functions.
494 -- @name luci.sys.process
497 --- Get the current process id.
499 -- @name process.info
500 -- @return Number containing the current pid
501 function process.info(key)
502 local s = {uid = nixio.getuid(), gid = nixio.getgid()}
503 return not key and s or s[key]
506 --- Retrieve information about currently running processes.
507 -- @return Table containing process information
508 function process.list()
511 local ps = luci.util.execi("top -bn1")
523 k = luci.util.split(luci.util.trim(line), "%s+", nil, true)
524 if k[1] == "PID" then
532 line = luci.util.trim(line)
533 for i, value in ipairs(luci.util.split(line, "%s+", #k-1, true)) do
537 local pid = tonumber(row[k[1]])
546 --- Set the gid of a process identified by given pid.
547 -- @param gid Number containing the Unix group id
548 -- @return Boolean indicating successful operation
549 -- @return String containing the error message if failed
550 -- @return Number containing the error code if failed
551 function process.setgroup(gid)
552 return nixio.setgid(gid)
555 --- Set the uid of a process identified by given pid.
556 -- @param uid Number containing the Unix user id
557 -- @return Boolean indicating successful operation
558 -- @return String containing the error message if failed
559 -- @return Number containing the error code if failed
560 function process.setuser(uid)
561 return nixio.setuid(uid)
564 --- Send a signal to a process identified by given pid.
566 -- @name process.signal
567 -- @param pid Number containing the process id
568 -- @param sig Signal to send (default: 15 [SIGTERM])
569 -- @return Boolean indicating successful operation
570 -- @return Number containing the error code if failed
571 process.signal = nixio.kill
574 --- LuCI system utilities / user related functions.
576 -- @name luci.sys.user
579 --- Retrieve user informations for given uid.
582 -- @param uid Number containing the Unix user id
583 -- @return Table containing the following fields:
584 -- { "uid", "gid", "name", "passwd", "dir", "shell", "gecos" }
585 user.getuser = nixio.getpw
587 --- Retrieve the current user password hash.
588 -- @param username String containing the username to retrieve the password for
589 -- @return String containing the hash or nil if no password is set.
590 function user.getpasswd(username)
591 local pwe = nixio.getsp and nixio.getsp(username) or nixio.getpw(username)
592 local pwh = pwe and (pwe.pwdp or pwe.passwd)
593 if not pwh or #pwh < 1 or pwh == "!" or pwh == "x" then
600 --- Test whether given string matches the password of a given system user.
601 -- @param username String containing the Unix user name
602 -- @param pass String containing the password to compare
603 -- @return Boolean indicating wheather the passwords are equal
604 function user.checkpasswd(username, pass)
605 local pwh = user.getpasswd(username)
606 if not pwh or nixio.crypt(pass, pwh) ~= pwh then
613 --- Change the password of given user.
614 -- @param username String containing the Unix user name
615 -- @param password String containing the password to compare
616 -- @return Number containing 0 on success and >= 1 on error
617 function user.setpasswd(username, password)
619 password = password:gsub("'", "")
623 username = username:gsub("'", "")
626 local cmd = "(echo '"..password.."';sleep 1;echo '"..password.."')|"
627 cmd = cmd .. "passwd '"..username.."' >/dev/null 2>&1"
628 return os.execute(cmd)
632 --- LuCI system utilities / wifi related functions.
634 -- @name luci.sys.wifi
637 --- Get wireless information for given interface.
638 -- @param ifname String containing the interface name
639 -- @return A wrapped iwinfo object instance
640 function wifi.getiwinfo(ifname)
641 local t = iwinfo.type(ifname)
644 return setmetatable({}, {
645 __index = function(t, k)
646 if x[k] then return x[k](ifname) end
652 --- Get iwconfig output for all wireless devices.
653 -- @return Table of tables containing the iwconfing output for each wifi device
654 function wifi.getiwconfig()
655 local cnt = luci.util.exec("PATH=/sbin:/usr/sbin iwconfig 2>/dev/null")
658 for i, l in pairs(luci.util.split(luci.util.trim(cnt), "\n\n")) do
659 local k = l:match("^(.-) ")
660 l = l:gsub("^(.-) +", "", 1)
662 local entry, flags = _parse_mixed_record(l)
673 --- Get iwlist scan output from all wireless devices.
674 -- @return Table of tables contaiing all scan results
675 function wifi.iwscan(iface)
676 local siface = iface or ""
677 local cnt = luci.util.exec("iwlist "..siface.." scan 2>/dev/null")
680 for i, l in pairs(luci.util.split(luci.util.trim(cnt), "\n\n")) do
681 local k = l:match("^(.-) ")
682 l = l:gsub("^[^\n]+", "", 1)
683 l = luci.util.trim(l)
686 for j, c in pairs(luci.util.split(l, "\n Cell")) do
687 c = c:gsub("^(.-)- ", "", 1)
688 c = luci.util.split(c, "\n", 7)
689 c = table.concat(c, "\n", 1)
690 local entry, flags = _parse_mixed_record(c)
694 table.insert(iws[k], entry)
699 return iface and (iws[iface] or {}) or iws
702 --- Get available channels from given wireless iface.
703 -- @param iface Wireless interface (optional)
704 -- @return Table of available channels
705 function wifi.channels(iface)
706 local t = iwinfo.type(iface or "")
708 if t and iwinfo[t] then
709 cns = iwinfo[t].freqlist(iface)
712 if not cns or #cns == 0 then
714 {channel = 1, mhz = 2412},
715 {channel = 2, mhz = 2417},
716 {channel = 3, mhz = 2422},
717 {channel = 4, mhz = 2427},
718 {channel = 5, mhz = 2432},
719 {channel = 6, mhz = 2437},
720 {channel = 7, mhz = 2442},
721 {channel = 8, mhz = 2447},
722 {channel = 9, mhz = 2452},
723 {channel = 10, mhz = 2457},
724 {channel = 11, mhz = 2462}
732 --- LuCI system utilities / init related functions.
734 -- @name luci.sys.init
736 init.dir = "/etc/init.d/"
738 --- Get the names of all installed init scripts
739 -- @return Table containing the names of all inistalled init scripts
740 function init.names()
742 for name in fs.glob(init.dir.."*") do
743 names[#names+1] = fs.basename(name)
748 --- Test whether the given init script is enabled
749 -- @param name Name of the init script
750 -- @return Boolean indicating whether init is enabled
751 function init.enabled(name)
752 if fs.access(init.dir..name) then
753 return ( call(init.dir..name.." enabled") == 0 )
758 --- Get the index of he given init script
759 -- @param name Name of the init script
760 -- @return Numeric index value
761 function init.index(name)
762 if fs.access(init.dir..name) then
763 return call("source "..init.dir..name.." enabled; exit $START")
767 --- Enable the given init script
768 -- @param name Name of the init script
769 -- @return Boolean indicating success
770 function init.enable(name)
771 if fs.access(init.dir..name) then
772 return ( call(init.dir..name.." enable") == 1 )
776 --- Disable the given init script
777 -- @param name Name of the init script
778 -- @return Boolean indicating success
779 function init.disable(name)
780 if fs.access(init.dir..name) then
781 return ( call(init.dir..name.." disable") == 0 )
786 -- Internal functions
788 function _parse_delimited_table(iter, delimiter, callback)
789 delimiter = delimiter or "%s+"
792 local trim = luci.util.trim
793 local split = luci.util.split
795 local keys = split(trim(iter()), delimiter, nil, true)
796 for i, j in pairs(keys) do
797 keys[i] = trim(keys[i])
804 for i, j in pairs(split(line, delimiter, nil, true)) do
821 function _parse_mixed_record(cnt, delimiter)
822 delimiter = delimiter or " "
826 for i, l in pairs(luci.util.split(luci.util.trim(cnt), "\n")) do
827 for j, f in pairs(luci.util.split(luci.util.trim(l), delimiter, nil, true)) do
828 local k, x, v = f:match('([^%s][^:=]*) *([:=]*) *"*([^\n"]*)"*')
832 table.insert(flags, k)