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"
34 local uci = require "luci.model.uci"
37 luci.util = require "luci.util"
38 luci.ip = require "luci.ip"
40 local tonumber, ipairs, pairs, pcall, type, next, setmetatable =
41 tonumber, ipairs, pairs, pcall, type, next, setmetatable
44 --- LuCI Linux and POSIX system utilities.
47 --- Execute a given shell command and return the error code
50 -- @param ... Command to call
51 -- @return Error code of the command
53 return os.execute(...) / 256
56 --- Execute a given shell command and capture its standard output
59 -- @param command Command to call
60 -- @return String containg the return the output of the command
63 --- Retrieve information about currently mounted file systems.
64 -- @return Table containing mount information
67 local k = {"fs", "blocks", "used", "available", "percent", "mountpoint"}
68 local ps = luci.util.execi("df")
80 for value in line:gmatch("[^%s]+") do
87 -- this is a rather ugly workaround to cope with wrapped lines in
90 -- /dev/scsi/host0/bus0/target0/lun0/part3
91 -- 114382024 93566472 15005244 86% /mnt/usb
97 for value in line:gmatch("[^%s]+") do
103 table.insert(data, row)
110 --- Retrieve environment variables. If no variable is given then a table
111 -- containing the whole environment is returned otherwise this function returns
112 -- the corresponding string value for the given name or nil if no such variable
116 -- @param var Name of the environment variable to retrieve (optional)
117 -- @return String containg the value of the specified variable
118 -- @return Table containing all variables if no variable name is given
119 getenv = nixio.getenv
121 --- Get or set the current hostname.
122 -- @param String containing a new hostname to set (optional)
123 -- @return String containing the system hostname
124 function hostname(newname)
125 if type(newname) == "string" and #newname > 0 then
126 fs.writefile( "/proc/sys/kernel/hostname", newname )
129 return nixio.uname().nodename
133 --- Returns the contents of a documented referred by an URL.
134 -- @param url The URL to retrieve
135 -- @param stream Return a stream instead of a buffer
136 -- @param target Directly write to target file name
137 -- @return String containing the contents of given the URL
138 function httpget(url, stream, target)
140 local source = stream and io.popen or luci.util.exec
141 return source("wget -qO- '"..url:gsub("'", "").."'")
143 return os.execute("wget -qO '%s' '%s'" %
144 {target:gsub("'", ""), url:gsub("'", "")})
148 --- Returns the system load average values.
149 -- @return String containing the average load value 1 minute ago
150 -- @return String containing the average load value 5 minutes ago
151 -- @return String containing the average load value 15 minutes ago
153 local info = nixio.sysinfo()
154 return info.loads[1], info.loads[2], info.loads[3]
157 --- Initiate a system reboot.
158 -- @return Return value of os.execute()
160 return os.execute("reboot >/dev/null 2>&1")
163 --- Returns the system type, cpu name and installed physical memory.
164 -- @return String containing the system or platform identifier
165 -- @return String containing hardware model information
166 -- @return String containing the total memory amount in kB
167 -- @return String containing the memory used for caching in kB
168 -- @return String containing the memory used for buffering in kB
169 -- @return String containing the free memory amount in kB
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+)"))
182 system = nixio.uname().machine
183 model = cpuinfo:match("model name.-:%s*([^\n]+)")
185 model = cpuinfo:match("Processor.-:%s*([^\n]+)")
188 model = cpuinfo:match("cpu model.-:%s*([^\n]+)")
191 return system, model, memtotal, memcached, membuffers, memfree
194 --- Retrieves the output of the "logread" command.
195 -- @return String containing the current log buffer
197 return luci.util.exec("logread")
200 --- Retrieves the output of the "dmesg" command.
201 -- @return String containing the current log buffer
203 return luci.util.exec("dmesg")
206 --- Generates a random id with specified length.
207 -- @param bytes Number of bytes for the unique id
208 -- @return String containing hex encoded id
209 function uniqueid(bytes)
210 local rand = fs.readfile("/dev/urandom", bytes)
211 return rand and nixio.bin.hexlify(rand)
214 --- Returns the current system uptime stats.
215 -- @return String containing total uptime in seconds
217 return nixio.sysinfo().uptime
221 --- LuCI system utilities / network related functions.
223 -- @name luci.sys.net
226 --- Returns the current arp-table entries as two-dimensional table.
227 -- @return Table of table containing the current arp entries.
228 -- The following fields are defined for arp entry objects:
229 -- { "IP address", "HW address", "HW type", "Flags", "Mask", "Device" }
230 function net.arptable(callback)
231 return _parse_delimited_table(io.lines("/proc/net/arp"), "%s%s+", callback)
234 --- Returns conntrack information
235 -- @return Table with the currently tracked IP connections
236 function net.conntrack(callback)
238 if fs.access("/proc/net/nf_conntrack", "r") then
239 for line in io.lines("/proc/net/nf_conntrack") do
240 line = line:match "^(.-( [^ =]+=).-)%2"
241 local entry, flags = _parse_mixed_record(line, " +")
242 entry.layer3 = flags[1]
243 entry.layer4 = flags[3]
251 connt[#connt+1] = entry
254 elseif fs.access("/proc/net/ip_conntrack", "r") then
255 for line in io.lines("/proc/net/ip_conntrack") do
256 line = line:match "^(.-( [^ =]+=).-)%2"
257 local entry, flags = _parse_mixed_record(line, " +")
258 entry.layer3 = "ipv4"
259 entry.layer4 = flags[1]
267 connt[#connt+1] = entry
276 --- Determine the current IPv4 default route. If multiple default routes exist,
277 -- return the one with the lowest metric.
278 -- @return Table with the properties of the current default route.
279 -- The following fields are defined:
280 -- { "dest", "gateway", "metric", "refcount", "usecount", "irtt",
281 -- "flags", "device" }
282 function net.defaultroute()
285 net.routes(function(rt)
286 if rt.dest:prefix() == 0 and (not route or route.metric > rt.metric) then
294 --- Determine the current IPv6 default route. If multiple default routes exist,
295 -- return the one with the lowest metric.
296 -- @return Table with the properties of the current default route.
297 -- The following fields are defined:
298 -- { "source", "dest", "nexthop", "metric", "refcount", "usecount",
299 -- "flags", "device" }
300 function net.defaultroute6()
303 net.routes6(function(rt)
304 if rt.dest:prefix() == 0 and (not route or route.metric > rt.metric) then
312 --- Determine the names of available network interfaces.
313 -- @return Table containing all current interface names
314 function net.devices()
316 for k, v in ipairs(nixio.getifaddrs()) do
317 if v.family == "packet" then
318 devs[#devs+1] = v.name
325 --- Return information about available network interfaces.
326 -- @return Table containing all current interface names and their information
327 function net.deviceinfo()
329 for k, v in ipairs(nixio.getifaddrs()) do
330 if v.family == "packet" then
355 -- Determine the MAC address belonging to the given IP address.
356 -- @param ip IPv4 address
357 -- @return String containing the MAC address or nil if it cannot be found
358 function net.ip4mac(ip)
360 net.arptable(function(e)
361 if e["IP address"] == ip then
362 mac = e["HW address"]
368 --- Returns the current kernel routing table entries.
369 -- @return Table of tables with properties of the corresponding routes.
370 -- The following fields are defined for route entry tables:
371 -- { "dest", "gateway", "metric", "refcount", "usecount", "irtt",
372 -- "flags", "device" }
373 function net.routes(callback)
376 for line in io.lines("/proc/net/route") do
378 local dev, dst_ip, gateway, flags, refcnt, usecnt, metric,
379 dst_mask, mtu, win, irtt = line:match(
380 "([^%s]+)\t([A-F0-9]+)\t([A-F0-9]+)\t([A-F0-9]+)\t" ..
381 "(%d+)\t(%d+)\t(%d+)\t([A-F0-9]+)\t(%d+)\t(%d+)\t(%d+)"
385 gateway = luci.ip.Hex( gateway, 32, luci.ip.FAMILY_INET4 )
386 dst_mask = luci.ip.Hex( dst_mask, 32, luci.ip.FAMILY_INET4 )
387 dst_ip = luci.ip.Hex(
388 dst_ip, dst_mask:prefix(dst_mask), luci.ip.FAMILY_INET4
394 metric = tonumber(metric),
395 refcount = tonumber(refcnt),
396 usecount = tonumber(usecnt),
398 window = tonumber(window),
399 irtt = tonumber(irtt),
400 flags = tonumber(flags, 16),
407 routes[#routes+1] = rt
415 --- Returns the current ipv6 kernel routing table entries.
416 -- @return Table of tables with properties of the corresponding routes.
417 -- The following fields are defined for route entry tables:
418 -- { "source", "dest", "nexthop", "metric", "refcount", "usecount",
419 -- "flags", "device" }
420 function net.routes6(callback)
421 if fs.access("/proc/net/ipv6_route", "r") then
424 for line in io.lines("/proc/net/ipv6_route") do
426 local dst_ip, dst_prefix, src_ip, src_prefix, nexthop,
427 metric, refcnt, usecnt, flags, dev = line:match(
428 "([a-f0-9]+) ([a-f0-9]+) " ..
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]+) +([^%s]+)"
435 src_ip = luci.ip.Hex(
436 src_ip, tonumber(src_prefix, 16), luci.ip.FAMILY_INET6, false
439 dst_ip = luci.ip.Hex(
440 dst_ip, tonumber(dst_prefix, 16), luci.ip.FAMILY_INET6, false
443 nexthop = luci.ip.Hex( nexthop, 128, luci.ip.FAMILY_INET6, false )
449 metric = tonumber(metric, 16),
450 refcount = tonumber(refcnt, 16),
451 usecount = tonumber(usecnt, 16),
452 flags = tonumber(flags, 16),
455 -- lua number is too small for storing the metric
456 -- add a metric_raw field with the original content
463 routes[#routes+1] = rt
471 --- Tests whether the given host responds to ping probes.
472 -- @param host String containing a hostname or IPv4 address
473 -- @return Number containing 0 on success and >= 1 on error
474 function net.pingtest(host)
475 return os.execute("ping -c1 '"..host:gsub("'", '').."' >/dev/null 2>&1")
479 --- LuCI system utilities / process related functions.
481 -- @name luci.sys.process
484 --- Get the current process id.
486 -- @name process.info
487 -- @return Number containing the current pid
488 function process.info(key)
489 local s = {uid = nixio.getuid(), gid = nixio.getgid()}
490 return not key and s or s[key]
493 --- Retrieve information about currently running processes.
494 -- @return Table containing process information
495 function process.list()
498 local ps = luci.util.execi("top -bn1")
510 k = luci.util.split(luci.util.trim(line), "%s+", nil, true)
511 if k[1] == "PID" then
519 line = luci.util.trim(line)
520 for i, value in ipairs(luci.util.split(line, "%s+", #k-1, true)) do
524 local pid = tonumber(row[k[1]])
533 --- Set the gid of a process identified by given pid.
534 -- @param gid Number containing the Unix group id
535 -- @return Boolean indicating successful operation
536 -- @return String containing the error message if failed
537 -- @return Number containing the error code if failed
538 function process.setgroup(gid)
539 return nixio.setgid(gid)
542 --- Set the uid of a process identified by given pid.
543 -- @param uid Number containing the Unix user id
544 -- @return Boolean indicating successful operation
545 -- @return String containing the error message if failed
546 -- @return Number containing the error code if failed
547 function process.setuser(uid)
548 return nixio.setuid(uid)
551 --- Send a signal to a process identified by given pid.
553 -- @name process.signal
554 -- @param pid Number containing the process id
555 -- @param sig Signal to send (default: 15 [SIGTERM])
556 -- @return Boolean indicating successful operation
557 -- @return Number containing the error code if failed
558 process.signal = nixio.kill
561 --- LuCI system utilities / user related functions.
563 -- @name luci.sys.user
566 --- Retrieve user informations for given uid.
569 -- @param uid Number containing the Unix user id
570 -- @return Table containing the following fields:
571 -- { "uid", "gid", "name", "passwd", "dir", "shell", "gecos" }
572 user.getuser = nixio.getpw
574 --- Retrieve the current user password hash.
575 -- @param username String containing the username to retrieve the password for
576 -- @return String containing the hash or nil if no password is set.
577 function user.getpasswd(username)
578 local pwe = nixio.getsp and nixio.getsp(username) or nixio.getpw(username)
579 local pwh = pwe and (pwe.pwdp or pwe.passwd)
580 if not pwh or #pwh < 1 or pwh == "!" or pwh == "x" then
587 --- Test whether given string matches the password of a given system user.
588 -- @param username String containing the Unix user name
589 -- @param pass String containing the password to compare
590 -- @return Boolean indicating wheather the passwords are equal
591 function user.checkpasswd(username, pass)
592 local pwh = user.getpasswd(username)
593 if pwh and nixio.crypt(pass, pwh) ~= pwh then
600 --- Change the password of given user.
601 -- @param username String containing the Unix user name
602 -- @param password String containing the password to compare
603 -- @return Number containing 0 on success and >= 1 on error
604 function user.setpasswd(username, password)
606 password = password:gsub("'", "")
610 username = username:gsub("'", "")
613 local cmd = "(echo '"..password.."';sleep 1;echo '"..password.."')|"
614 cmd = cmd .. "passwd '"..username.."' >/dev/null 2>&1"
615 return os.execute(cmd)
619 --- LuCI system utilities / wifi related functions.
621 -- @name luci.sys.wifi
624 --- Get wireless information for given interface.
625 -- @param ifname String containing the interface name
626 -- @return A wrapped iwinfo object instance
627 function wifi.getiwinfo(ifname)
630 local u = uci.cursor_state()
631 local d, n = ifname:match("^(%w+)%.network(%d+)")
634 u:foreach("wireless", "wifi-iface",
636 if s.device == d then
639 ifname = s.ifname or s.device
644 elseif u:get("wireless", ifname) == "wifi-device" then
645 u:foreach("wireless", "wifi-iface",
647 if s.device == ifname and s.ifname then
654 local t = iwinfo.type(ifname)
657 return setmetatable({}, {
658 __index = function(t, k)
659 if k == "ifname" then
670 --- Get iwconfig output for all wireless devices.
671 -- @return Table of tables containing the iwconfing output for each wifi device
672 function wifi.getiwconfig()
673 local cnt = luci.util.exec("PATH=/sbin:/usr/sbin iwconfig 2>/dev/null")
676 for i, l in pairs(luci.util.split(luci.util.trim(cnt), "\n\n")) do
677 local k = l:match("^(.-) ")
678 l = l:gsub("^(.-) +", "", 1)
680 local entry, flags = _parse_mixed_record(l)
691 --- Get iwlist scan output from all wireless devices.
692 -- @return Table of tables contaiing all scan results
693 function wifi.iwscan(iface)
694 local siface = iface or ""
695 local cnt = luci.util.exec("iwlist "..siface.." scan 2>/dev/null")
698 for i, l in pairs(luci.util.split(luci.util.trim(cnt), "\n\n")) do
699 local k = l:match("^(.-) ")
700 l = l:gsub("^[^\n]+", "", 1)
701 l = luci.util.trim(l)
704 for j, c in pairs(luci.util.split(l, "\n Cell")) do
705 c = c:gsub("^(.-)- ", "", 1)
706 c = luci.util.split(c, "\n", 7)
707 c = table.concat(c, "\n", 1)
708 local entry, flags = _parse_mixed_record(c)
712 table.insert(iws[k], entry)
717 return iface and (iws[iface] or {}) or iws
720 --- Get available channels from given wireless iface.
721 -- @param iface Wireless interface (optional)
722 -- @return Table of available channels
723 function wifi.channels(iface)
724 local t = iwinfo.type(iface or "")
726 if iface and t and iwinfo[t] then
727 cns = iwinfo[t].freqlist(iface)
730 if not cns or #cns == 0 then
732 {channel = 1, mhz = 2412},
733 {channel = 2, mhz = 2417},
734 {channel = 3, mhz = 2422},
735 {channel = 4, mhz = 2427},
736 {channel = 5, mhz = 2432},
737 {channel = 6, mhz = 2437},
738 {channel = 7, mhz = 2442},
739 {channel = 8, mhz = 2447},
740 {channel = 9, mhz = 2452},
741 {channel = 10, mhz = 2457},
742 {channel = 11, mhz = 2462}
750 --- LuCI system utilities / init related functions.
752 -- @name luci.sys.init
754 init.dir = "/etc/init.d/"
756 --- Get the names of all installed init scripts
757 -- @return Table containing the names of all inistalled init scripts
758 function init.names()
760 for name in fs.glob(init.dir.."*") do
761 names[#names+1] = fs.basename(name)
766 --- Test whether the given init script is enabled
767 -- @param name Name of the init script
768 -- @return Boolean indicating whether init is enabled
769 function init.enabled(name)
770 if fs.access(init.dir..name) then
771 return ( call(init.dir..name.." enabled") == 0 )
776 --- Get the index of he given init script
777 -- @param name Name of the init script
778 -- @return Numeric index value
779 function init.index(name)
780 if fs.access(init.dir..name) then
781 return call("source "..init.dir..name.." enabled; exit $START")
785 --- Enable the given init script
786 -- @param name Name of the init script
787 -- @return Boolean indicating success
788 function init.enable(name)
789 if fs.access(init.dir..name) then
790 return ( call(init.dir..name.." enable") == 1 )
794 --- Disable the given init script
795 -- @param name Name of the init script
796 -- @return Boolean indicating success
797 function init.disable(name)
798 if fs.access(init.dir..name) then
799 return ( call(init.dir..name.." disable") == 0 )
804 -- Internal functions
806 function _parse_delimited_table(iter, delimiter, callback)
807 delimiter = delimiter or "%s+"
810 local trim = luci.util.trim
811 local split = luci.util.split
813 local keys = split(trim(iter()), delimiter, nil, true)
814 for i, j in pairs(keys) do
815 keys[i] = trim(keys[i])
822 for i, j in pairs(split(line, delimiter, nil, true)) do
839 function _parse_mixed_record(cnt, delimiter)
840 delimiter = delimiter or " "
844 for i, l in pairs(luci.util.split(luci.util.trim(cnt), "\n")) do
845 for j, f in pairs(luci.util.split(luci.util.trim(l), delimiter, nil, true)) do
846 local k, x, v = f:match('([^%s][^:=]*) *([:=]*) *"*([^\n"]*)"*')
850 table.insert(flags, k)