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 memtotal = tonumber(meminfo:match("MemTotal:%s*(%d+)"))
175 local memcached = tonumber(meminfo:match("\nCached:%s*(%d+)"))
176 local memfree = tonumber(meminfo:match("MemFree:%s*(%d+)"))
177 local membuffers = tonumber(meminfo:match("Buffers:%s*(%d+)"))
178 local bogomips = tonumber(cpuinfo:match("[Bb]ogo[Mm][Ii][Pp][Ss].-: ([^\n]+)")) or 0
181 cpuinfo:match("system type\t+: ([^\n]+)") or
182 cpuinfo:match("Processor\t+: ([^\n]+)") or
183 cpuinfo:match("model name\t+: ([^\n]+)")
186 cpuinfo:match("machine\t+: ([^\n]+)") or
187 cpuinfo:match("Hardware\t+: ([^\n]+)") or
188 luci.util.pcdata(fs.readfile("/proc/diag/model")) or
189 nixio.uname().machine or
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 if flags[6] ~= "TIME_WAIT" then
244 entry.layer3 = flags[1]
245 entry.layer4 = flags[3]
253 connt[#connt+1] = entry
257 elseif fs.access("/proc/net/ip_conntrack", "r") then
258 for line in io.lines("/proc/net/ip_conntrack") do
259 line = line:match "^(.-( [^ =]+=).-)%2"
260 local entry, flags = _parse_mixed_record(line, " +")
261 if flags[4] ~= "TIME_WAIT" then
262 entry.layer3 = "ipv4"
263 entry.layer4 = flags[1]
271 connt[#connt+1] = entry
281 --- Determine the current IPv4 default route. If multiple default routes exist,
282 -- return the one with the lowest metric.
283 -- @return Table with the properties of the current default route.
284 -- The following fields are defined:
285 -- { "dest", "gateway", "metric", "refcount", "usecount", "irtt",
286 -- "flags", "device" }
287 function net.defaultroute()
290 net.routes(function(rt)
291 if rt.dest:prefix() == 0 and (not route or route.metric > rt.metric) then
299 --- Determine the current IPv6 default route. If multiple default routes exist,
300 -- return the one with the lowest metric.
301 -- @return Table with the properties of the current default route.
302 -- The following fields are defined:
303 -- { "source", "dest", "nexthop", "metric", "refcount", "usecount",
304 -- "flags", "device" }
305 function net.defaultroute6()
308 net.routes6(function(rt)
309 if rt.dest:prefix() == 0 and rt.device ~= "lo" and
310 (not route or route.metric > rt.metric)
317 local global_unicast = luci.ip.IPv6("2000::/3")
318 net.routes6(function(rt)
319 if rt.dest:equal(global_unicast) and
320 (not route or route.metric > rt.metric)
330 --- Determine the names of available network interfaces.
331 -- @return Table containing all current interface names
332 function net.devices()
334 for k, v in ipairs(nixio.getifaddrs()) do
335 if v.family == "packet" then
336 devs[#devs+1] = v.name
343 --- Return information about available network interfaces.
344 -- @return Table containing all current interface names and their information
345 function net.deviceinfo()
347 for k, v in ipairs(nixio.getifaddrs()) do
348 if v.family == "packet" then
373 -- Determine the MAC address belonging to the given IP address.
374 -- @param ip IPv4 address
375 -- @return String containing the MAC address or nil if it cannot be found
376 function net.ip4mac(ip)
378 net.arptable(function(e)
379 if e["IP address"] == ip then
380 mac = e["HW address"]
386 --- Returns the current kernel routing table entries.
387 -- @return Table of tables with properties of the corresponding routes.
388 -- The following fields are defined for route entry tables:
389 -- { "dest", "gateway", "metric", "refcount", "usecount", "irtt",
390 -- "flags", "device" }
391 function net.routes(callback)
394 for line in io.lines("/proc/net/route") do
396 local dev, dst_ip, gateway, flags, refcnt, usecnt, metric,
397 dst_mask, mtu, win, irtt = line:match(
398 "([^%s]+)\t([A-F0-9]+)\t([A-F0-9]+)\t([A-F0-9]+)\t" ..
399 "(%d+)\t(%d+)\t(%d+)\t([A-F0-9]+)\t(%d+)\t(%d+)\t(%d+)"
403 gateway = luci.ip.Hex( gateway, 32, luci.ip.FAMILY_INET4 )
404 dst_mask = luci.ip.Hex( dst_mask, 32, luci.ip.FAMILY_INET4 )
405 dst_ip = luci.ip.Hex(
406 dst_ip, dst_mask:prefix(dst_mask), luci.ip.FAMILY_INET4
412 metric = tonumber(metric),
413 refcount = tonumber(refcnt),
414 usecount = tonumber(usecnt),
416 window = tonumber(window),
417 irtt = tonumber(irtt),
418 flags = tonumber(flags, 16),
425 routes[#routes+1] = rt
433 --- Returns the current ipv6 kernel routing table entries.
434 -- @return Table of tables with properties of the corresponding routes.
435 -- The following fields are defined for route entry tables:
436 -- { "source", "dest", "nexthop", "metric", "refcount", "usecount",
437 -- "flags", "device" }
438 function net.routes6(callback)
439 if fs.access("/proc/net/ipv6_route", "r") then
442 for line in io.lines("/proc/net/ipv6_route") do
444 local dst_ip, dst_prefix, src_ip, src_prefix, nexthop,
445 metric, refcnt, usecnt, flags, dev = line:match(
446 "([a-f0-9]+) ([a-f0-9]+) " ..
447 "([a-f0-9]+) ([a-f0-9]+) " ..
448 "([a-f0-9]+) ([a-f0-9]+) " ..
449 "([a-f0-9]+) ([a-f0-9]+) " ..
450 "([a-f0-9]+) +([^%s]+)"
453 src_ip = luci.ip.Hex(
454 src_ip, tonumber(src_prefix, 16), luci.ip.FAMILY_INET6, false
457 dst_ip = luci.ip.Hex(
458 dst_ip, tonumber(dst_prefix, 16), luci.ip.FAMILY_INET6, false
461 nexthop = luci.ip.Hex( nexthop, 128, luci.ip.FAMILY_INET6, false )
467 metric = tonumber(metric, 16),
468 refcount = tonumber(refcnt, 16),
469 usecount = tonumber(usecnt, 16),
470 flags = tonumber(flags, 16),
473 -- lua number is too small for storing the metric
474 -- add a metric_raw field with the original content
481 routes[#routes+1] = rt
489 --- Tests whether the given host responds to ping probes.
490 -- @param host String containing a hostname or IPv4 address
491 -- @return Number containing 0 on success and >= 1 on error
492 function net.pingtest(host)
493 return os.execute("ping -c1 '"..host:gsub("'", '').."' >/dev/null 2>&1")
497 --- LuCI system utilities / process related functions.
499 -- @name luci.sys.process
502 --- Get the current process id.
504 -- @name process.info
505 -- @return Number containing the current pid
506 function process.info(key)
507 local s = {uid = nixio.getuid(), gid = nixio.getgid()}
508 return not key and s or s[key]
511 --- Retrieve information about currently running processes.
512 -- @return Table containing process information
513 function process.list()
516 local ps = luci.util.execi("top -bn1")
528 k = luci.util.split(luci.util.trim(line), "%s+", nil, true)
529 if k[1] == "PID" then
537 line = luci.util.trim(line)
538 for i, value in ipairs(luci.util.split(line, "%s+", #k-1, true)) do
542 local pid = tonumber(row[k[1]])
551 --- Set the gid of a process identified by given pid.
552 -- @param gid Number containing the Unix group id
553 -- @return Boolean indicating successful operation
554 -- @return String containing the error message if failed
555 -- @return Number containing the error code if failed
556 function process.setgroup(gid)
557 return nixio.setgid(gid)
560 --- Set the uid of a process identified by given pid.
561 -- @param uid Number containing the Unix user id
562 -- @return Boolean indicating successful operation
563 -- @return String containing the error message if failed
564 -- @return Number containing the error code if failed
565 function process.setuser(uid)
566 return nixio.setuid(uid)
569 --- Send a signal to a process identified by given pid.
571 -- @name process.signal
572 -- @param pid Number containing the process id
573 -- @param sig Signal to send (default: 15 [SIGTERM])
574 -- @return Boolean indicating successful operation
575 -- @return Number containing the error code if failed
576 process.signal = nixio.kill
579 --- LuCI system utilities / user related functions.
581 -- @name luci.sys.user
584 --- Retrieve user informations for given uid.
587 -- @param uid Number containing the Unix user id
588 -- @return Table containing the following fields:
589 -- { "uid", "gid", "name", "passwd", "dir", "shell", "gecos" }
590 user.getuser = nixio.getpw
592 --- Retrieve the current user password hash.
593 -- @param username String containing the username to retrieve the password for
594 -- @return String containing the hash or nil if no password is set.
595 function user.getpasswd(username)
596 local pwe = nixio.getsp and nixio.getsp(username) or nixio.getpw(username)
597 local pwh = pwe and (pwe.pwdp or pwe.passwd)
598 if not pwh or #pwh < 1 or pwh == "!" or pwh == "x" then
605 --- Test whether given string matches the password of a given system user.
606 -- @param username String containing the Unix user name
607 -- @param pass String containing the password to compare
608 -- @return Boolean indicating wheather the passwords are equal
609 function user.checkpasswd(username, pass)
610 local pwh = user.getpasswd(username)
611 if pwh and nixio.crypt(pass, pwh) ~= pwh then
618 --- Change the password of given user.
619 -- @param username String containing the Unix user name
620 -- @param password String containing the password to compare
621 -- @return Number containing 0 on success and >= 1 on error
622 function user.setpasswd(username, password)
624 password = password:gsub("'", [['"'"']])
628 username = username:gsub("'", [['"'"']])
632 "(echo '" .. password .. "'; sleep 1; echo '" .. password .. "') | " ..
633 "passwd '" .. username .. "' >/dev/null 2>&1"
638 --- LuCI system utilities / wifi related functions.
640 -- @name luci.sys.wifi
643 --- Get wireless information for given interface.
644 -- @param ifname String containing the interface name
645 -- @return A wrapped iwinfo object instance
646 function wifi.getiwinfo(ifname)
647 local stat, iwinfo = pcall(require, "iwinfo")
651 local u = uci.cursor_state()
652 local d, n = ifname:match("^(%w+)%.network(%d+)")
655 u:foreach("wireless", "wifi-iface",
657 if s.device == d then
660 ifname = s.ifname or s.device
665 elseif u:get("wireless", ifname) == "wifi-device" then
666 u:foreach("wireless", "wifi-iface",
668 if s.device == ifname and s.ifname then
675 local t = stat and iwinfo.type(ifname)
676 local x = t and iwinfo[t] or { }
677 return setmetatable({}, {
678 __index = function(t, k)
679 if k == "ifname" then
689 --- Get iwconfig output for all wireless devices.
690 -- @return Table of tables containing the iwconfing output for each wifi device
691 function wifi.getiwconfig()
692 local cnt = luci.util.exec("PATH=/sbin:/usr/sbin iwconfig 2>/dev/null")
695 for i, l in pairs(luci.util.split(luci.util.trim(cnt), "\n\n")) do
696 local k = l:match("^(.-) ")
697 l = l:gsub("^(.-) +", "", 1)
699 local entry, flags = _parse_mixed_record(l)
710 --- Get iwlist scan output from all wireless devices.
711 -- @return Table of tables contaiing all scan results
712 function wifi.iwscan(iface)
713 local siface = iface or ""
714 local cnt = luci.util.exec("iwlist "..siface.." scan 2>/dev/null")
717 for i, l in pairs(luci.util.split(luci.util.trim(cnt), "\n\n")) do
718 local k = l:match("^(.-) ")
719 l = l:gsub("^[^\n]+", "", 1)
720 l = luci.util.trim(l)
723 for j, c in pairs(luci.util.split(l, "\n Cell")) do
724 c = c:gsub("^(.-)- ", "", 1)
725 c = luci.util.split(c, "\n", 7)
726 c = table.concat(c, "\n", 1)
727 local entry, flags = _parse_mixed_record(c)
731 table.insert(iws[k], entry)
736 return iface and (iws[iface] or {}) or iws
739 --- Get available channels from given wireless iface.
740 -- @param iface Wireless interface (optional)
741 -- @return Table of available channels
742 function wifi.channels(iface)
743 local stat, iwinfo = pcall(require, "iwinfo")
747 local t = iwinfo.type(iface or "")
748 if iface and t and iwinfo[t] then
749 cns = iwinfo[t].freqlist(iface)
753 if not cns or #cns == 0 then
755 {channel = 1, mhz = 2412},
756 {channel = 2, mhz = 2417},
757 {channel = 3, mhz = 2422},
758 {channel = 4, mhz = 2427},
759 {channel = 5, mhz = 2432},
760 {channel = 6, mhz = 2437},
761 {channel = 7, mhz = 2442},
762 {channel = 8, mhz = 2447},
763 {channel = 9, mhz = 2452},
764 {channel = 10, mhz = 2457},
765 {channel = 11, mhz = 2462}
773 --- LuCI system utilities / init related functions.
775 -- @name luci.sys.init
777 init.dir = "/etc/init.d/"
779 --- Get the names of all installed init scripts
780 -- @return Table containing the names of all inistalled init scripts
781 function init.names()
783 for name in fs.glob(init.dir.."*") do
784 names[#names+1] = fs.basename(name)
789 --- Test whether the given init script is enabled
790 -- @param name Name of the init script
791 -- @return Boolean indicating whether init is enabled
792 function init.enabled(name)
793 if fs.access(init.dir..name) then
794 return ( call(init.dir..name.." enabled") == 0 )
799 --- Get the index of he given init script
800 -- @param name Name of the init script
801 -- @return Numeric index value
802 function init.index(name)
803 if fs.access(init.dir..name) then
804 return call("source "..init.dir..name.." enabled; exit $START")
808 --- Enable the given init script
809 -- @param name Name of the init script
810 -- @return Boolean indicating success
811 function init.enable(name)
812 if fs.access(init.dir..name) then
813 return ( call(init.dir..name.." enable") == 1 )
817 --- Disable the given init script
818 -- @param name Name of the init script
819 -- @return Boolean indicating success
820 function init.disable(name)
821 if fs.access(init.dir..name) then
822 return ( call(init.dir..name.." disable") == 0 )
827 -- Internal functions
829 function _parse_delimited_table(iter, delimiter, callback)
830 delimiter = delimiter or "%s+"
833 local trim = luci.util.trim
834 local split = luci.util.split
836 local keys = split(trim(iter()), delimiter, nil, true)
837 for i, j in pairs(keys) do
838 keys[i] = trim(keys[i])
845 for i, j in pairs(split(line, delimiter, nil, true)) do
862 function _parse_mixed_record(cnt, delimiter)
863 delimiter = delimiter or " "
867 for i, l in pairs(luci.util.split(luci.util.trim(cnt), "\n")) do
868 for j, f in pairs(luci.util.split(luci.util.trim(l), delimiter, nil, true)) do
869 local k, x, v = f:match('([^%s][^:=]*) *([:=]*) *"*([^\n"]*)"*')
873 table.insert(flags, k)