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 luci.util.pcdata(fs.readfile("/tmp/sysinfo/model")) or
187 cpuinfo:match("machine\t+: ([^\n]+)") or
188 cpuinfo:match("Hardware\t+: ([^\n]+)") or
189 luci.util.pcdata(fs.readfile("/proc/diag/model")) or
190 nixio.uname().machine or
193 return system, model, memtotal, memcached, membuffers, memfree, bogomips
196 --- Retrieves the output of the "logread" command.
197 -- @return String containing the current log buffer
199 return luci.util.exec("logread")
202 --- Retrieves the output of the "dmesg" command.
203 -- @return String containing the current log buffer
205 return luci.util.exec("dmesg")
208 --- Generates a random id with specified length.
209 -- @param bytes Number of bytes for the unique id
210 -- @return String containing hex encoded id
211 function uniqueid(bytes)
212 local rand = fs.readfile("/dev/urandom", bytes)
213 return rand and nixio.bin.hexlify(rand)
216 --- Returns the current system uptime stats.
217 -- @return String containing total uptime in seconds
219 return nixio.sysinfo().uptime
223 --- LuCI system utilities / network related functions.
225 -- @name luci.sys.net
228 --- Returns the current arp-table entries as two-dimensional table.
229 -- @return Table of table containing the current arp entries.
230 -- The following fields are defined for arp entry objects:
231 -- { "IP address", "HW address", "HW type", "Flags", "Mask", "Device" }
232 function net.arptable(callback)
233 return _parse_delimited_table(io.lines("/proc/net/arp"), "%s%s+", callback)
236 --- Returns conntrack information
237 -- @return Table with the currently tracked IP connections
238 function net.conntrack(callback)
240 if fs.access("/proc/net/nf_conntrack", "r") then
241 for line in io.lines("/proc/net/nf_conntrack") do
242 line = line:match "^(.-( [^ =]+=).-)%2"
243 local entry, flags = _parse_mixed_record(line, " +")
244 if flags[6] ~= "TIME_WAIT" then
245 entry.layer3 = flags[1]
246 entry.layer4 = flags[3]
254 connt[#connt+1] = entry
258 elseif fs.access("/proc/net/ip_conntrack", "r") then
259 for line in io.lines("/proc/net/ip_conntrack") do
260 line = line:match "^(.-( [^ =]+=).-)%2"
261 local entry, flags = _parse_mixed_record(line, " +")
262 if flags[4] ~= "TIME_WAIT" then
263 entry.layer3 = "ipv4"
264 entry.layer4 = flags[1]
272 connt[#connt+1] = entry
282 --- Determine the current IPv4 default route. If multiple default routes exist,
283 -- return the one with the lowest metric.
284 -- @return Table with the properties of the current default route.
285 -- The following fields are defined:
286 -- { "dest", "gateway", "metric", "refcount", "usecount", "irtt",
287 -- "flags", "device" }
288 function net.defaultroute()
291 net.routes(function(rt)
292 if rt.dest:prefix() == 0 and (not route or route.metric > rt.metric) then
300 --- Determine the current IPv6 default route. If multiple default routes exist,
301 -- return the one with the lowest metric.
302 -- @return Table with the properties of the current default route.
303 -- The following fields are defined:
304 -- { "source", "dest", "nexthop", "metric", "refcount", "usecount",
305 -- "flags", "device" }
306 function net.defaultroute6()
309 net.routes6(function(rt)
310 if rt.dest:prefix() == 0 and rt.device ~= "lo" and
311 (not route or route.metric > rt.metric)
318 local global_unicast = luci.ip.IPv6("2000::/3")
319 net.routes6(function(rt)
320 if rt.dest:equal(global_unicast) and
321 (not route or route.metric > rt.metric)
331 --- Determine the names of available network interfaces.
332 -- @return Table containing all current interface names
333 function net.devices()
335 for k, v in ipairs(nixio.getifaddrs()) do
336 if v.family == "packet" then
337 devs[#devs+1] = v.name
344 --- Return information about available network interfaces.
345 -- @return Table containing all current interface names and their information
346 function net.deviceinfo()
348 for k, v in ipairs(nixio.getifaddrs()) do
349 if v.family == "packet" then
374 -- Determine the MAC address belonging to the given IP address.
375 -- @param ip IPv4 address
376 -- @return String containing the MAC address or nil if it cannot be found
377 function net.ip4mac(ip)
379 net.arptable(function(e)
380 if e["IP address"] == ip then
381 mac = e["HW address"]
387 --- Returns the current kernel routing table entries.
388 -- @return Table of tables with properties of the corresponding routes.
389 -- The following fields are defined for route entry tables:
390 -- { "dest", "gateway", "metric", "refcount", "usecount", "irtt",
391 -- "flags", "device" }
392 function net.routes(callback)
395 for line in io.lines("/proc/net/route") do
397 local dev, dst_ip, gateway, flags, refcnt, usecnt, metric,
398 dst_mask, mtu, win, irtt = line:match(
399 "([^%s]+)\t([A-F0-9]+)\t([A-F0-9]+)\t([A-F0-9]+)\t" ..
400 "(%d+)\t(%d+)\t(%d+)\t([A-F0-9]+)\t(%d+)\t(%d+)\t(%d+)"
404 gateway = luci.ip.Hex( gateway, 32, luci.ip.FAMILY_INET4 )
405 dst_mask = luci.ip.Hex( dst_mask, 32, luci.ip.FAMILY_INET4 )
406 dst_ip = luci.ip.Hex(
407 dst_ip, dst_mask:prefix(dst_mask), luci.ip.FAMILY_INET4
413 metric = tonumber(metric),
414 refcount = tonumber(refcnt),
415 usecount = tonumber(usecnt),
417 window = tonumber(window),
418 irtt = tonumber(irtt),
419 flags = tonumber(flags, 16),
426 routes[#routes+1] = rt
434 --- Returns the current ipv6 kernel routing table entries.
435 -- @return Table of tables with properties of the corresponding routes.
436 -- The following fields are defined for route entry tables:
437 -- { "source", "dest", "nexthop", "metric", "refcount", "usecount",
438 -- "flags", "device" }
439 function net.routes6(callback)
440 if fs.access("/proc/net/ipv6_route", "r") then
443 for line in io.lines("/proc/net/ipv6_route") do
445 local dst_ip, dst_prefix, src_ip, src_prefix, nexthop,
446 metric, refcnt, usecnt, flags, dev = line:match(
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]+) ([a-f0-9]+) " ..
451 "([a-f0-9]+) +([^%s]+)"
454 src_ip = luci.ip.Hex(
455 src_ip, tonumber(src_prefix, 16), luci.ip.FAMILY_INET6, false
458 dst_ip = luci.ip.Hex(
459 dst_ip, tonumber(dst_prefix, 16), luci.ip.FAMILY_INET6, false
462 nexthop = luci.ip.Hex( nexthop, 128, luci.ip.FAMILY_INET6, false )
468 metric = tonumber(metric, 16),
469 refcount = tonumber(refcnt, 16),
470 usecount = tonumber(usecnt, 16),
471 flags = tonumber(flags, 16),
474 -- lua number is too small for storing the metric
475 -- add a metric_raw field with the original content
482 routes[#routes+1] = rt
490 --- Tests whether the given host responds to ping probes.
491 -- @param host String containing a hostname or IPv4 address
492 -- @return Number containing 0 on success and >= 1 on error
493 function net.pingtest(host)
494 return os.execute("ping -c1 '"..host:gsub("'", '').."' >/dev/null 2>&1")
498 --- LuCI system utilities / process related functions.
500 -- @name luci.sys.process
503 --- Get the current process id.
505 -- @name process.info
506 -- @return Number containing the current pid
507 function process.info(key)
508 local s = {uid = nixio.getuid(), gid = nixio.getgid()}
509 return not key and s or s[key]
512 --- Retrieve information about currently running processes.
513 -- @return Table containing process information
514 function process.list()
517 local ps = luci.util.execi("top -bn1")
529 k = luci.util.split(luci.util.trim(line), "%s+", nil, true)
530 if k[6] == "%VSZ" then
533 if k[1] == "PID" then
541 line = luci.util.trim(line)
542 for i, value in ipairs(luci.util.split(line, "%s+", #k-1, true)) do
546 local pid = tonumber(row[k[1]])
555 --- Set the gid of a process identified by given pid.
556 -- @param gid Number containing the Unix group 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.setgroup(gid)
561 return nixio.setgid(gid)
564 --- Set the uid of a process identified by given pid.
565 -- @param uid Number containing the Unix user id
566 -- @return Boolean indicating successful operation
567 -- @return String containing the error message if failed
568 -- @return Number containing the error code if failed
569 function process.setuser(uid)
570 return nixio.setuid(uid)
573 --- Send a signal to a process identified by given pid.
575 -- @name process.signal
576 -- @param pid Number containing the process id
577 -- @param sig Signal to send (default: 15 [SIGTERM])
578 -- @return Boolean indicating successful operation
579 -- @return Number containing the error code if failed
580 process.signal = nixio.kill
583 --- LuCI system utilities / user related functions.
585 -- @name luci.sys.user
588 --- Retrieve user informations for given uid.
591 -- @param uid Number containing the Unix user id
592 -- @return Table containing the following fields:
593 -- { "uid", "gid", "name", "passwd", "dir", "shell", "gecos" }
594 user.getuser = nixio.getpw
596 --- Retrieve the current user password hash.
597 -- @param username String containing the username to retrieve the password for
598 -- @return String containing the hash or nil if no password is set.
599 function user.getpasswd(username)
600 local pwe = nixio.getsp and nixio.getsp(username) or nixio.getpw(username)
601 local pwh = pwe and (pwe.pwdp or pwe.passwd)
602 if not pwh or #pwh < 1 or pwh == "!" or pwh == "x" then
609 --- Test whether given string matches the password of a given system user.
610 -- @param username String containing the Unix user name
611 -- @param pass String containing the password to compare
612 -- @return Boolean indicating wheather the passwords are equal
613 function user.checkpasswd(username, pass)
614 local pwh = user.getpasswd(username)
615 if pwh and nixio.crypt(pass, pwh) ~= pwh then
622 --- Change the password of given user.
623 -- @param username String containing the Unix user name
624 -- @param password String containing the password to compare
625 -- @return Number containing 0 on success and >= 1 on error
626 function user.setpasswd(username, password)
628 password = password:gsub("'", [['"'"']])
632 username = username:gsub("'", [['"'"']])
636 "(echo '" .. password .. "'; sleep 1; echo '" .. password .. "') | " ..
637 "passwd '" .. username .. "' >/dev/null 2>&1"
642 --- LuCI system utilities / wifi related functions.
644 -- @name luci.sys.wifi
647 --- Get wireless information for given interface.
648 -- @param ifname String containing the interface name
649 -- @return A wrapped iwinfo object instance
650 function wifi.getiwinfo(ifname)
651 local stat, iwinfo = pcall(require, "iwinfo")
655 local u = uci.cursor_state()
656 local d, n = ifname:match("^(%w+)%.network(%d+)")
659 u:foreach("wireless", "wifi-iface",
661 if s.device == d then
664 ifname = s.ifname or s.device
669 elseif u:get("wireless", ifname) == "wifi-device" then
670 u:foreach("wireless", "wifi-iface",
672 if s.device == ifname and s.ifname then
679 local t = stat and iwinfo.type(ifname)
680 local x = t and iwinfo[t] or { }
681 return setmetatable({}, {
682 __index = function(t, k)
683 if k == "ifname" then
693 --- Get iwconfig output for all wireless devices.
694 -- @return Table of tables containing the iwconfing output for each wifi device
695 function wifi.getiwconfig()
696 local cnt = luci.util.exec("PATH=/sbin:/usr/sbin iwconfig 2>/dev/null")
699 for i, l in pairs(luci.util.split(luci.util.trim(cnt), "\n\n")) do
700 local k = l:match("^(.-) ")
701 l = l:gsub("^(.-) +", "", 1)
703 local entry, flags = _parse_mixed_record(l)
714 --- Get iwlist scan output from all wireless devices.
715 -- @return Table of tables contaiing all scan results
716 function wifi.iwscan(iface)
717 local siface = iface or ""
718 local cnt = luci.util.exec("iwlist "..siface.." scan 2>/dev/null")
721 for i, l in pairs(luci.util.split(luci.util.trim(cnt), "\n\n")) do
722 local k = l:match("^(.-) ")
723 l = l:gsub("^[^\n]+", "", 1)
724 l = luci.util.trim(l)
727 for j, c in pairs(luci.util.split(l, "\n Cell")) do
728 c = c:gsub("^(.-)- ", "", 1)
729 c = luci.util.split(c, "\n", 7)
730 c = table.concat(c, "\n", 1)
731 local entry, flags = _parse_mixed_record(c)
735 table.insert(iws[k], entry)
740 return iface and (iws[iface] or {}) or iws
743 --- Get available channels from given wireless iface.
744 -- @param iface Wireless interface (optional)
745 -- @return Table of available channels
746 function wifi.channels(iface)
747 local stat, iwinfo = pcall(require, "iwinfo")
751 local t = iwinfo.type(iface or "")
752 if iface and t and iwinfo[t] then
753 cns = iwinfo[t].freqlist(iface)
757 if not cns or #cns == 0 then
759 {channel = 1, mhz = 2412},
760 {channel = 2, mhz = 2417},
761 {channel = 3, mhz = 2422},
762 {channel = 4, mhz = 2427},
763 {channel = 5, mhz = 2432},
764 {channel = 6, mhz = 2437},
765 {channel = 7, mhz = 2442},
766 {channel = 8, mhz = 2447},
767 {channel = 9, mhz = 2452},
768 {channel = 10, mhz = 2457},
769 {channel = 11, mhz = 2462}
777 --- LuCI system utilities / init related functions.
779 -- @name luci.sys.init
781 init.dir = "/etc/init.d/"
783 --- Get the names of all installed init scripts
784 -- @return Table containing the names of all inistalled init scripts
785 function init.names()
787 for name in fs.glob(init.dir.."*") do
788 names[#names+1] = fs.basename(name)
793 --- Get the index of he given init script
794 -- @param name Name of the init script
795 -- @return Numeric index value
796 function init.index(name)
797 if fs.access(init.dir..name) then
798 return call("env -i sh -c 'source %s%s enabled; exit ${START:-255}' >/dev/null"
803 local function init_action(action, name)
804 if fs.access(init.dir..name) then
805 return call("env -i %s%s %s >/dev/null" %{ init.dir, name, action })
809 --- Test whether the given init script is enabled
810 -- @param name Name of the init script
811 -- @return Boolean indicating whether init is enabled
812 function init.enabled(name)
813 return (init_action("enabled", name) == 0)
816 --- Enable the given init script
817 -- @param name Name of the init script
818 -- @return Boolean indicating success
819 function init.enable(name)
820 return (init_action("enable", name) == 1)
823 --- Disable the given init script
824 -- @param name Name of the init script
825 -- @return Boolean indicating success
826 function init.disable(name)
827 return (init_action("disable", name) == 0)
830 --- Start the given init script
831 -- @param name Name of the init script
832 -- @return Boolean indicating success
833 function init.start(name)
834 return (init_action("start", name) == 0)
837 --- Stop the given init script
838 -- @param name Name of the init script
839 -- @return Boolean indicating success
840 function init.stop(name)
841 return (init_action("stop", name) == 0)
845 -- Internal functions
847 function _parse_delimited_table(iter, delimiter, callback)
848 delimiter = delimiter or "%s+"
851 local trim = luci.util.trim
852 local split = luci.util.split
854 local keys = split(trim(iter()), delimiter, nil, true)
855 for i, j in pairs(keys) do
856 keys[i] = trim(keys[i])
863 for i, j in pairs(split(line, delimiter, nil, true)) do
880 function _parse_mixed_record(cnt, delimiter)
881 delimiter = delimiter or " "
885 for i, l in pairs(luci.util.split(luci.util.trim(cnt), "\n")) do
886 for j, f in pairs(luci.util.split(luci.util.trim(l), delimiter, nil, true)) do
887 local k, x, v = f:match('([^%s][^:=]*) *([:=]*) *"*([^\n"]*)"*')
891 table.insert(flags, k)