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
169 -- @return String containing the cpu bogomips (number)
171 local cpuinfo = fs.readfile("/proc/cpuinfo")
172 local meminfo = fs.readfile("/proc/meminfo")
174 local system = cpuinfo:match("system typ.-:%s*([^\n]+)")
176 local memtotal = tonumber(meminfo:match("MemTotal:%s*(%d+)"))
177 local memcached = tonumber(meminfo:match("\nCached:%s*(%d+)"))
178 local memfree = tonumber(meminfo:match("MemFree:%s*(%d+)"))
179 local membuffers = tonumber(meminfo:match("Buffers:%s*(%d+)"))
180 local bogomips = tonumber(cpuinfo:match("BogoMIPS.-:%s*([^\n]+)"))
183 system = nixio.uname().machine
184 model = cpuinfo:match("model name.-:%s*([^\n]+)")
186 model = cpuinfo:match("Processor.-:%s*([^\n]+)")
189 model = cpuinfo:match("cpu model.-:%s*([^\n]+)")
192 return system, model, memtotal, memcached, membuffers, memfree, bogomips
195 --- Retrieves the output of the "logread" command.
196 -- @return String containing the current log buffer
198 return luci.util.exec("logread")
201 --- Retrieves the output of the "dmesg" command.
202 -- @return String containing the current log buffer
204 return luci.util.exec("dmesg")
207 --- Generates a random id with specified length.
208 -- @param bytes Number of bytes for the unique id
209 -- @return String containing hex encoded id
210 function uniqueid(bytes)
211 local rand = fs.readfile("/dev/urandom", bytes)
212 return rand and nixio.bin.hexlify(rand)
215 --- Returns the current system uptime stats.
216 -- @return String containing total uptime in seconds
218 return nixio.sysinfo().uptime
222 --- LuCI system utilities / network related functions.
224 -- @name luci.sys.net
227 --- Returns the current arp-table entries as two-dimensional table.
228 -- @return Table of table containing the current arp entries.
229 -- The following fields are defined for arp entry objects:
230 -- { "IP address", "HW address", "HW type", "Flags", "Mask", "Device" }
231 function net.arptable(callback)
232 return _parse_delimited_table(io.lines("/proc/net/arp"), "%s%s+", callback)
235 --- Returns conntrack information
236 -- @return Table with the currently tracked IP connections
237 function net.conntrack(callback)
239 if fs.access("/proc/net/nf_conntrack", "r") then
240 for line in io.lines("/proc/net/nf_conntrack") do
241 line = line:match "^(.-( [^ =]+=).-)%2"
242 local entry, flags = _parse_mixed_record(line, " +")
243 entry.layer3 = flags[1]
244 entry.layer4 = flags[3]
252 connt[#connt+1] = entry
255 elseif fs.access("/proc/net/ip_conntrack", "r") then
256 for line in io.lines("/proc/net/ip_conntrack") do
257 line = line:match "^(.-( [^ =]+=).-)%2"
258 local entry, flags = _parse_mixed_record(line, " +")
259 entry.layer3 = "ipv4"
260 entry.layer4 = flags[1]
268 connt[#connt+1] = entry
277 --- Determine the current IPv4 default route. If multiple default routes exist,
278 -- return the one with the lowest metric.
279 -- @return Table with the properties of the current default route.
280 -- The following fields are defined:
281 -- { "dest", "gateway", "metric", "refcount", "usecount", "irtt",
282 -- "flags", "device" }
283 function net.defaultroute()
286 net.routes(function(rt)
287 if rt.dest:prefix() == 0 and (not route or route.metric > rt.metric) then
295 --- Determine the current IPv6 default route. If multiple default routes exist,
296 -- return the one with the lowest metric.
297 -- @return Table with the properties of the current default route.
298 -- The following fields are defined:
299 -- { "source", "dest", "nexthop", "metric", "refcount", "usecount",
300 -- "flags", "device" }
301 function net.defaultroute6()
304 net.routes6(function(rt)
305 if rt.dest:prefix() == 0 and (not route or route.metric > rt.metric) then
313 --- Determine the names of available network interfaces.
314 -- @return Table containing all current interface names
315 function net.devices()
317 for k, v in ipairs(nixio.getifaddrs()) do
318 if v.family == "packet" then
319 devs[#devs+1] = v.name
326 --- Return information about available network interfaces.
327 -- @return Table containing all current interface names and their information
328 function net.deviceinfo()
330 for k, v in ipairs(nixio.getifaddrs()) do
331 if v.family == "packet" then
356 -- Determine the MAC address belonging to the given IP address.
357 -- @param ip IPv4 address
358 -- @return String containing the MAC address or nil if it cannot be found
359 function net.ip4mac(ip)
361 net.arptable(function(e)
362 if e["IP address"] == ip then
363 mac = e["HW address"]
369 --- Returns the current kernel routing table entries.
370 -- @return Table of tables with properties of the corresponding routes.
371 -- The following fields are defined for route entry tables:
372 -- { "dest", "gateway", "metric", "refcount", "usecount", "irtt",
373 -- "flags", "device" }
374 function net.routes(callback)
377 for line in io.lines("/proc/net/route") do
379 local dev, dst_ip, gateway, flags, refcnt, usecnt, metric,
380 dst_mask, mtu, win, irtt = line:match(
381 "([^%s]+)\t([A-F0-9]+)\t([A-F0-9]+)\t([A-F0-9]+)\t" ..
382 "(%d+)\t(%d+)\t(%d+)\t([A-F0-9]+)\t(%d+)\t(%d+)\t(%d+)"
386 gateway = luci.ip.Hex( gateway, 32, luci.ip.FAMILY_INET4 )
387 dst_mask = luci.ip.Hex( dst_mask, 32, luci.ip.FAMILY_INET4 )
388 dst_ip = luci.ip.Hex(
389 dst_ip, dst_mask:prefix(dst_mask), luci.ip.FAMILY_INET4
395 metric = tonumber(metric),
396 refcount = tonumber(refcnt),
397 usecount = tonumber(usecnt),
399 window = tonumber(window),
400 irtt = tonumber(irtt),
401 flags = tonumber(flags, 16),
408 routes[#routes+1] = rt
416 --- Returns the current ipv6 kernel routing table entries.
417 -- @return Table of tables with properties of the corresponding routes.
418 -- The following fields are defined for route entry tables:
419 -- { "source", "dest", "nexthop", "metric", "refcount", "usecount",
420 -- "flags", "device" }
421 function net.routes6(callback)
422 if fs.access("/proc/net/ipv6_route", "r") then
425 for line in io.lines("/proc/net/ipv6_route") do
427 local dst_ip, dst_prefix, src_ip, src_prefix, nexthop,
428 metric, refcnt, usecnt, flags, dev = line:match(
429 "([a-f0-9]+) ([a-f0-9]+) " ..
430 "([a-f0-9]+) ([a-f0-9]+) " ..
431 "([a-f0-9]+) ([a-f0-9]+) " ..
432 "([a-f0-9]+) ([a-f0-9]+) " ..
433 "([a-f0-9]+) +([^%s]+)"
436 src_ip = luci.ip.Hex(
437 src_ip, tonumber(src_prefix, 16), luci.ip.FAMILY_INET6, false
440 dst_ip = luci.ip.Hex(
441 dst_ip, tonumber(dst_prefix, 16), luci.ip.FAMILY_INET6, false
444 nexthop = luci.ip.Hex( nexthop, 128, luci.ip.FAMILY_INET6, false )
450 metric = tonumber(metric, 16),
451 refcount = tonumber(refcnt, 16),
452 usecount = tonumber(usecnt, 16),
453 flags = tonumber(flags, 16),
456 -- lua number is too small for storing the metric
457 -- add a metric_raw field with the original content
464 routes[#routes+1] = rt
472 --- Tests whether the given host responds to ping probes.
473 -- @param host String containing a hostname or IPv4 address
474 -- @return Number containing 0 on success and >= 1 on error
475 function net.pingtest(host)
476 return os.execute("ping -c1 '"..host:gsub("'", '').."' >/dev/null 2>&1")
480 --- LuCI system utilities / process related functions.
482 -- @name luci.sys.process
485 --- Get the current process id.
487 -- @name process.info
488 -- @return Number containing the current pid
489 function process.info(key)
490 local s = {uid = nixio.getuid(), gid = nixio.getgid()}
491 return not key and s or s[key]
494 --- Retrieve information about currently running processes.
495 -- @return Table containing process information
496 function process.list()
499 local ps = luci.util.execi("top -bn1")
511 k = luci.util.split(luci.util.trim(line), "%s+", nil, true)
512 if k[1] == "PID" then
520 line = luci.util.trim(line)
521 for i, value in ipairs(luci.util.split(line, "%s+", #k-1, true)) do
525 local pid = tonumber(row[k[1]])
534 --- Set the gid of a process identified by given pid.
535 -- @param gid Number containing the Unix group id
536 -- @return Boolean indicating successful operation
537 -- @return String containing the error message if failed
538 -- @return Number containing the error code if failed
539 function process.setgroup(gid)
540 return nixio.setgid(gid)
543 --- Set the uid of a process identified by given pid.
544 -- @param uid Number containing the Unix user id
545 -- @return Boolean indicating successful operation
546 -- @return String containing the error message if failed
547 -- @return Number containing the error code if failed
548 function process.setuser(uid)
549 return nixio.setuid(uid)
552 --- Send a signal to a process identified by given pid.
554 -- @name process.signal
555 -- @param pid Number containing the process id
556 -- @param sig Signal to send (default: 15 [SIGTERM])
557 -- @return Boolean indicating successful operation
558 -- @return Number containing the error code if failed
559 process.signal = nixio.kill
562 --- LuCI system utilities / user related functions.
564 -- @name luci.sys.user
567 --- Retrieve user informations for given uid.
570 -- @param uid Number containing the Unix user id
571 -- @return Table containing the following fields:
572 -- { "uid", "gid", "name", "passwd", "dir", "shell", "gecos" }
573 user.getuser = nixio.getpw
575 --- Retrieve the current user password hash.
576 -- @param username String containing the username to retrieve the password for
577 -- @return String containing the hash or nil if no password is set.
578 function user.getpasswd(username)
579 local pwe = nixio.getsp and nixio.getsp(username) or nixio.getpw(username)
580 local pwh = pwe and (pwe.pwdp or pwe.passwd)
581 if not pwh or #pwh < 1 or pwh == "!" or pwh == "x" then
588 --- Test whether given string matches the password of a given system user.
589 -- @param username String containing the Unix user name
590 -- @param pass String containing the password to compare
591 -- @return Boolean indicating wheather the passwords are equal
592 function user.checkpasswd(username, pass)
593 local pwh = user.getpasswd(username)
594 if pwh and nixio.crypt(pass, pwh) ~= pwh then
601 --- Change the password of given user.
602 -- @param username String containing the Unix user name
603 -- @param password String containing the password to compare
604 -- @return Number containing 0 on success and >= 1 on error
605 function user.setpasswd(username, password)
607 password = password:gsub("'", [['"'"']])
611 username = username:gsub("'", [['"'"']])
615 "(echo '" .. password .. "'; sleep 1; echo '" .. password .. "') | " ..
616 "passwd '" .. username .. "' >/dev/null 2>&1"
621 --- LuCI system utilities / wifi related functions.
623 -- @name luci.sys.wifi
626 --- Get wireless information for given interface.
627 -- @param ifname String containing the interface name
628 -- @return A wrapped iwinfo object instance
629 function wifi.getiwinfo(ifname)
630 local stat, iwinfo = pcall(require, "iwinfo")
634 local u = uci.cursor_state()
635 local d, n = ifname:match("^(%w+)%.network(%d+)")
638 u:foreach("wireless", "wifi-iface",
640 if s.device == d then
643 ifname = s.ifname or s.device
648 elseif u:get("wireless", ifname) == "wifi-device" then
649 u:foreach("wireless", "wifi-iface",
651 if s.device == ifname and s.ifname then
658 local t = stat and iwinfo.type(ifname)
659 local x = t and iwinfo[t] or { }
660 return setmetatable({}, {
661 __index = function(t, k)
662 if k == "ifname" then
672 --- Get iwconfig output for all wireless devices.
673 -- @return Table of tables containing the iwconfing output for each wifi device
674 function wifi.getiwconfig()
675 local cnt = luci.util.exec("PATH=/sbin:/usr/sbin iwconfig 2>/dev/null")
678 for i, l in pairs(luci.util.split(luci.util.trim(cnt), "\n\n")) do
679 local k = l:match("^(.-) ")
680 l = l:gsub("^(.-) +", "", 1)
682 local entry, flags = _parse_mixed_record(l)
693 --- Get iwlist scan output from all wireless devices.
694 -- @return Table of tables contaiing all scan results
695 function wifi.iwscan(iface)
696 local siface = iface or ""
697 local cnt = luci.util.exec("iwlist "..siface.." scan 2>/dev/null")
700 for i, l in pairs(luci.util.split(luci.util.trim(cnt), "\n\n")) do
701 local k = l:match("^(.-) ")
702 l = l:gsub("^[^\n]+", "", 1)
703 l = luci.util.trim(l)
706 for j, c in pairs(luci.util.split(l, "\n Cell")) do
707 c = c:gsub("^(.-)- ", "", 1)
708 c = luci.util.split(c, "\n", 7)
709 c = table.concat(c, "\n", 1)
710 local entry, flags = _parse_mixed_record(c)
714 table.insert(iws[k], entry)
719 return iface and (iws[iface] or {}) or iws
722 --- Get available channels from given wireless iface.
723 -- @param iface Wireless interface (optional)
724 -- @return Table of available channels
725 function wifi.channels(iface)
726 local stat, iwinfo = pcall(require, "iwinfo")
730 local t = iwinfo.type(iface or "")
731 if iface and t and iwinfo[t] then
732 cns = iwinfo[t].freqlist(iface)
736 if not cns or #cns == 0 then
738 {channel = 1, mhz = 2412},
739 {channel = 2, mhz = 2417},
740 {channel = 3, mhz = 2422},
741 {channel = 4, mhz = 2427},
742 {channel = 5, mhz = 2432},
743 {channel = 6, mhz = 2437},
744 {channel = 7, mhz = 2442},
745 {channel = 8, mhz = 2447},
746 {channel = 9, mhz = 2452},
747 {channel = 10, mhz = 2457},
748 {channel = 11, mhz = 2462}
756 --- LuCI system utilities / init related functions.
758 -- @name luci.sys.init
760 init.dir = "/etc/init.d/"
762 --- Get the names of all installed init scripts
763 -- @return Table containing the names of all inistalled init scripts
764 function init.names()
766 for name in fs.glob(init.dir.."*") do
767 names[#names+1] = fs.basename(name)
772 --- Test whether the given init script is enabled
773 -- @param name Name of the init script
774 -- @return Boolean indicating whether init is enabled
775 function init.enabled(name)
776 if fs.access(init.dir..name) then
777 return ( call(init.dir..name.." enabled") == 0 )
782 --- Get the index of he given init script
783 -- @param name Name of the init script
784 -- @return Numeric index value
785 function init.index(name)
786 if fs.access(init.dir..name) then
787 return call("source "..init.dir..name.." enabled; exit $START")
791 --- Enable the given init script
792 -- @param name Name of the init script
793 -- @return Boolean indicating success
794 function init.enable(name)
795 if fs.access(init.dir..name) then
796 return ( call(init.dir..name.." enable") == 1 )
800 --- Disable the given init script
801 -- @param name Name of the init script
802 -- @return Boolean indicating success
803 function init.disable(name)
804 if fs.access(init.dir..name) then
805 return ( call(init.dir..name.." disable") == 0 )
810 -- Internal functions
812 function _parse_delimited_table(iter, delimiter, callback)
813 delimiter = delimiter or "%s+"
816 local trim = luci.util.trim
817 local split = luci.util.split
819 local keys = split(trim(iter()), delimiter, nil, true)
820 for i, j in pairs(keys) do
821 keys[i] = trim(keys[i])
828 for i, j in pairs(split(line, delimiter, nil, true)) do
845 function _parse_mixed_record(cnt, delimiter)
846 delimiter = delimiter or " "
850 for i, l in pairs(luci.util.split(luci.util.trim(cnt), "\n")) do
851 for j, f in pairs(luci.util.split(luci.util.trim(l), delimiter, nil, true)) do
852 local k, x, v = f:match('([^%s][^:=]*) *([:=]*) *"*([^\n"]*)"*')
856 table.insert(flags, k)