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 nixio = require "nixio"
31 local table = require "table"
34 luci.util = require "luci.util"
35 luci.fs = require "luci.fs"
36 luci.ip = require "luci.ip"
38 local tonumber, ipairs, pairs, pcall, type =
39 tonumber, ipairs, pairs, pcall, type
42 --- LuCI Linux and POSIX system utilities.
45 --- Execute a given shell command and return the error code
48 -- @param ... Command to call
49 -- @return Error code of the command
51 return os.execute(...) / 256
54 --- Execute a given shell command and capture its standard output
57 -- @param command Command to call
58 -- @return String containg the return the output of the command
61 --- Invoke the luci-flash executable to write an image to the flash memory.
62 -- @param image Local path or URL to image file
63 -- @param kpattern Pattern of files to keep over flash process
64 -- @return Return value of os.execute()
65 function flash(image, kpattern)
66 local cmd = "luci-flash "
68 cmd = cmd .. "-k '" .. kpattern:gsub("'", "") .. "' "
70 cmd = cmd .. "'" .. image:gsub("'", "") .. "' >/dev/null 2>&1"
72 return os.execute(cmd)
75 --- Retrieve information about currently mounted file systems.
76 -- @return Table containing mount information
79 local k = {"fs", "blocks", "used", "available", "percent", "mountpoint"}
80 local ps = luci.util.execi("df")
92 for value in line:gmatch("[^%s]+") do
99 -- this is a rather ugly workaround to cope with wrapped lines in
102 -- /dev/scsi/host0/bus0/target0/lun0/part3
103 -- 114382024 93566472 15005244 86% /mnt/usb
106 if not row[k[2]] then
109 for value in line:gmatch("[^%s]+") do
115 table.insert(data, row)
122 --- Retrieve environment variables. If no variable is given then a table
123 -- containing the whole environment is returned otherwise this function returns
124 -- the corresponding string value for the given name or nil if no such variable
128 -- @param var Name of the environment variable to retrieve (optional)
129 -- @return String containg the value of the specified variable
130 -- @return Table containing all variables if no variable name is given
131 getenv = nixio.getenv
133 --- Get or set the current hostname.
134 -- @param String containing a new hostname to set (optional)
135 -- @return String containing the system hostname
136 function hostname(newname)
137 if type(newname) == "string" and #newname > 0 then
138 luci.fs.writefile( "/proc/sys/kernel/hostname", newname .. "\n" )
141 return nixio.uname().nodename
145 --- Returns the contents of a documented referred by an URL.
146 -- @param url The URL to retrieve
147 -- @param stream Return a stream instead of a buffer
148 -- @param target Directly write to target file name
149 -- @return String containing the contents of given the URL
150 function httpget(url, stream, target)
152 local source = stream and io.popen or luci.util.exec
153 return source("wget -qO- '"..url:gsub("'", "").."'")
155 return os.execute("wget -qO '%s' '%s'" %
156 {target:gsub("'", ""), url:gsub("'", "")})
160 --- Returns the system load average values.
161 -- @return String containing the average load value 1 minute ago
162 -- @return String containing the average load value 5 minutes ago
163 -- @return String containing the average load value 15 minutes ago
165 local info = nixio.sysinfo()
166 return info.loads[1], info.loads[2], info.loads[3]
169 --- Initiate a system reboot.
170 -- @return Return value of os.execute()
172 return os.execute("reboot >/dev/null 2>&1")
175 --- Returns the system type, cpu name and installed physical memory.
176 -- @return String containing the system or platform identifier
177 -- @return String containing hardware model information
178 -- @return String containing the total memory amount in kB
179 -- @return String containing the memory used for caching in kB
180 -- @return String containing the memory used for buffering in kB
181 -- @return String containing the free memory amount in kB
183 local cpuinfo = luci.fs.readfile("/proc/cpuinfo")
184 local meminfo = luci.fs.readfile("/proc/meminfo")
186 local system = cpuinfo:match("system typ.-:%s*([^\n]+)")
188 local memtotal = tonumber(meminfo:match("MemTotal:%s*(%d+)"))
189 local memcached = tonumber(meminfo:match("\nCached:%s*(%d+)"))
190 local memfree = tonumber(meminfo:match("MemFree:%s*(%d+)"))
191 local membuffers = tonumber(meminfo:match("Buffers:%s*(%d+)"))
194 system = nixio.uname().machine
195 model = cpuinfo:match("model name.-:%s*([^\n]+)")
197 model = cpuinfo:match("Processor.-:%s*([^\n]+)")
200 model = cpuinfo:match("cpu model.-:%s*([^\n]+)")
203 return system, model, memtotal, memcached, membuffers, memfree
206 --- Retrieves the output of the "logread" command.
207 -- @return String containing the current log buffer
209 return luci.util.exec("logread")
212 --- Retrieves the output of the "dmesg" command.
213 -- @return String containing the current log buffer
215 return luci.util.exec("dmesg")
218 --- Generates a random id with specified length.
219 -- @param bytes Number of bytes for the unique id
220 -- @return String containing hex encoded id
221 function uniqueid(bytes)
222 local fp = io.open("/dev/urandom")
223 local chunk = { fp:read(bytes):byte(1, bytes) }
228 local pattern = "%02X"
229 for i, byte in ipairs(chunk) do
230 hex = hex .. pattern:format(byte)
236 --- Returns the current system uptime stats.
237 -- @return String containing total uptime in seconds
238 -- @return String containing idle time in seconds
240 local loadavg = io.lines("/proc/uptime")()
241 return loadavg:match("^(.-) (.-)$")
245 --- LuCI system utilities / network related functions.
247 -- @name luci.sys.net
250 --- Returns the current arp-table entries as two-dimensional table.
251 -- @return Table of table containing the current arp entries.
252 -- The following fields are defined for arp entry objects:
253 -- { "IP address", "HW address", "HW type", "Flags", "Mask", "Device" }
254 function net.arptable()
255 return _parse_delimited_table(io.lines("/proc/net/arp"), "%s%s+")
258 --- Returns conntrack information
259 -- @return Table with the currently tracked IP connections
260 function net.conntrack()
262 if luci.fs.access("/proc/net/nf_conntrack", "r") then
263 for line in io.lines("/proc/net/nf_conntrack") do
264 line = line:match "^(.-( [^ =]+=).-)%2"
265 local entry, flags = _parse_mixed_record(line, " +")
266 entry.layer3 = flags[1]
267 entry.layer4 = flags[3]
272 connt[#connt+1] = entry
274 elseif luci.fs.access("/proc/net/ip_conntrack", "r") then
275 for line in io.lines("/proc/net/ip_conntrack") do
276 line = line:match "^(.-( [^ =]+=).-)%2"
277 local entry, flags = _parse_mixed_record(line, " +")
278 entry.layer3 = "ipv4"
279 entry.layer4 = flags[1]
284 connt[#connt+1] = entry
292 --- Determine the current IPv4 default route. If multiple default routes exist,
293 -- return the one with the lowest metric.
294 -- @return Table with the properties of the current default route.
295 -- The following fields are defined:
296 -- { "dest", "gateway", "metric", "refcount", "usecount", "irtt",
297 -- "flags", "device" }
298 function net.defaultroute()
300 for _, r in pairs(net.routes()) do
301 if r.dest:prefix() == 0 and (not route or route.metric > r.metric) then
308 --- Determine the current IPv6 default route. If multiple default routes exist,
309 -- return the one with the lowest metric.
310 -- @return Table with the properties of the current default route.
311 -- The following fields are defined:
312 -- { "source", "dest", "nexthop", "metric", "refcount", "usecount",
313 -- "flags", "device" }
314 function net.defaultroute6()
316 local routes6 = net.routes6()
318 for _, r in pairs(routes6) do
319 if r.dest:prefix() == 0 and
320 (not route or route.metric > r.metric)
329 --- Determine the names of available network interfaces.
330 -- @return Table containing all current interface names
331 function net.devices()
333 for line in io.lines("/proc/net/dev") do
334 table.insert(devices, line:match(" *(.-):"))
340 --- Return information about available network interfaces.
341 -- @return Table containing all current interface names and their information
342 function net.deviceinfo()
344 for line in io.lines("/proc/net/dev") do
345 local name, data = line:match("^ *(.-): *(.*)$")
346 if name and data then
347 devices[name] = luci.util.split(data, " +", nil, true)
354 -- Determine the MAC address belonging to the given IP address.
355 -- @param ip IPv4 address
356 -- @return String containing the MAC address or nil if it cannot be found
357 function net.ip4mac(ip)
360 for i, l in ipairs(net.arptable()) do
361 if l["IP address"] == ip then
362 mac = l["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()
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
392 routes[#routes+1] = {
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),
410 --- Returns the current ipv6 kernel routing table entries.
411 -- @return Table of tables with properties of the corresponding routes.
412 -- The following fields are defined for route entry tables:
413 -- { "source", "dest", "nexthop", "metric", "refcount", "usecount",
414 -- "flags", "device" }
415 function net.routes6()
416 if luci.fs.access("/proc/net/ipv6_route", "r") then
419 for line in io.lines("/proc/net/ipv6_route") do
421 local dst_ip, dst_prefix, src_ip, src_prefix, nexthop,
422 metric, refcnt, usecnt, flags, dev = line:match(
423 "([a-f0-9]+) ([a-f0-9]+) " ..
424 "([a-f0-9]+) ([a-f0-9]+) " ..
425 "([a-f0-9]+) ([a-f0-9]+) " ..
426 "([a-f0-9]+) ([a-f0-9]+) " ..
427 "([a-f0-9]+) +([^%s]+)"
430 src_ip = luci.ip.Hex(
431 src_ip, tonumber(src_prefix, 16), luci.ip.FAMILY_INET6, false
434 dst_ip = luci.ip.Hex(
435 dst_ip, tonumber(dst_prefix, 16), luci.ip.FAMILY_INET6, false
438 nexthop = luci.ip.Hex( nexthop, 128, luci.ip.FAMILY_INET6, false )
440 routes[#routes+1] = {
444 metric = tonumber(metric, 16),
445 refcount = tonumber(refcnt, 16),
446 usecount = tonumber(usecnt, 16),
447 flags = tonumber(flags, 16),
456 --- Tests whether the given host responds to ping probes.
457 -- @param host String containing a hostname or IPv4 address
458 -- @return Number containing 0 on success and >= 1 on error
459 function net.pingtest(host)
460 return os.execute("ping -c1 '"..host:gsub("'", '').."' >/dev/null 2>&1")
464 --- LuCI system utilities / process related functions.
466 -- @name luci.sys.process
469 --- Get the current process id.
471 -- @name process.info
472 -- @return Number containing the current pid
473 function process.info(key)
474 local s = {uid = nixio.getuid(), gid = nixio.getgid()}
475 return not key and s or s[key]
478 --- Retrieve information about currently running processes.
479 -- @return Table containing process information
480 function process.list()
483 local ps = luci.util.execi("top -bn1")
495 k = luci.util.split(luci.util.trim(line), "%s+", nil, true)
496 if k[1] == "PID" then
504 line = luci.util.trim(line)
505 for i, value in ipairs(luci.util.split(line, "%s+", #k-1, true)) do
509 local pid = tonumber(row[k[1]])
518 --- Set the gid of a process identified by given pid.
519 -- @param gid Number containing the Unix group id
520 -- @return Boolean indicating successful operation
521 -- @return String containing the error message if failed
522 -- @return Number containing the error code if failed
523 function process.setgroup(gid)
524 return nixio.setgid(gid)
527 --- Set the uid of a process identified by given pid.
528 -- @param uid Number containing the Unix user id
529 -- @return Boolean indicating successful operation
530 -- @return String containing the error message if failed
531 -- @return Number containing the error code if failed
532 function process.setuser(uid)
533 return nixio.setuid(uid)
536 --- Send a signal to a process identified by given pid.
538 -- @name process.signal
539 -- @param pid Number containing the process id
540 -- @param sig Signal to send (default: 15 [SIGTERM])
541 -- @return Boolean indicating successful operation
542 -- @return Number containing the error code if failed
543 process.signal = nixio.kill
546 --- LuCI system utilities / user related functions.
548 -- @name luci.sys.user
551 --- Retrieve user informations for given uid.
554 -- @param uid Number containing the Unix user id
555 -- @return Table containing the following fields:
556 -- { "uid", "gid", "name", "passwd", "dir", "shell", "gecos" }
557 user.getuser = nixio.getpw
559 --- Test whether given string matches the password of a given system user.
560 -- @param username String containing the Unix user name
561 -- @param pass String containing the password to compare
562 -- @return Boolean indicating wheather the passwords are equal
563 function user.checkpasswd(username, pass)
564 local pwe = nixio.getsp and nixio.getsp(username) or nixio.getpw(username)
565 local pwh = pwe and (pwe.pwdp or pwe.passwd)
566 if not pwh or #pwh < 1 or pwh ~= "!" and nixio.crypt(pass, pwh) ~= pwh then
573 --- Change the password of given user.
574 -- @param username String containing the Unix user name
575 -- @param password String containing the password to compare
576 -- @return Number containing 0 on success and >= 1 on error
577 function user.setpasswd(username, password)
579 password = password:gsub("'", "")
583 username = username:gsub("'", "")
586 local cmd = "(echo '"..password.."';sleep 1;echo '"..password.."')|"
587 cmd = cmd .. "passwd '"..username.."' >/dev/null 2>&1"
588 return os.execute(cmd)
592 --- LuCI system utilities / wifi related functions.
594 -- @name luci.sys.wifi
597 --- Get iwconfig output for all wireless devices.
598 -- @return Table of tables containing the iwconfing output for each wifi device
599 function wifi.getiwconfig()
600 local cnt = luci.util.exec("PATH=/sbin:/usr/sbin iwconfig 2>/dev/null")
603 for i, l in pairs(luci.util.split(luci.util.trim(cnt), "\n\n")) do
604 local k = l:match("^(.-) ")
605 l = l:gsub("^(.-) +", "", 1)
607 local entry, flags = _parse_mixed_record(l)
618 --- Get iwlist scan output from all wireless devices.
619 -- @return Table of tables contaiing all scan results
620 function wifi.iwscan(iface)
621 local siface = iface or ""
622 local cnt = luci.util.exec("iwlist "..siface.." scan 2>/dev/null")
625 for i, l in pairs(luci.util.split(luci.util.trim(cnt), "\n\n")) do
626 local k = l:match("^(.-) ")
627 l = l:gsub("^[^\n]+", "", 1)
628 l = luci.util.trim(l)
631 for j, c in pairs(luci.util.split(l, "\n Cell")) do
632 c = c:gsub("^(.-)- ", "", 1)
633 c = luci.util.split(c, "\n", 7)
634 c = table.concat(c, "\n", 1)
635 local entry, flags = _parse_mixed_record(c)
639 table.insert(iws[k], entry)
644 return iface and (iws[iface] or {}) or iws
647 --- Get available channels from given wireless iface.
648 -- @param iface Wireless interface (optional)
649 -- @return Table of available channels
650 function wifi.channels(iface)
651 local cmd = "iwlist " .. ( iface or "" ) .. " freq 2>/dev/null"
654 local fd = io.popen(cmd)
659 if not ln then break end
660 c, f = ln:match("Channel (%d+) : (%d+%.%d+) GHz")
662 cns[tonumber(c)] = tonumber(f)
668 if not ((pairs(cns))(cns)) then
670 2.412, 2.417, 2.422, 2.427, 2.432, 2.437,
671 2.442, 2.447, 2.452, 2.457, 2.462
679 --- LuCI system utilities / init related functions.
681 -- @name luci.sys.init
683 init.dir = "/etc/init.d/"
685 --- Get the names of all installed init scripts
686 -- @return Table containing the names of all inistalled init scripts
687 function init.names()
689 for _, name in ipairs(luci.fs.glob(init.dir.."*")) do
690 names[#names+1] = luci.fs.basename(name)
695 --- Test whether the given init script is enabled
696 -- @param name Name of the init script
697 -- @return Boolean indicating whether init is enabled
698 function init.enabled(name)
699 if luci.fs.access(init.dir..name) then
700 return ( call(init.dir..name.." enabled") == 0 )
705 --- Get the index of he given init script
706 -- @param name Name of the init script
707 -- @return Numeric index value
708 function init.index(name)
709 if luci.fs.access(init.dir..name) then
710 return call("source "..init.dir..name.."; exit $START")
714 --- Enable the given init script
715 -- @param name Name of the init script
716 -- @return Boolean indicating success
717 function init.enable(name)
718 if luci.fs.access(init.dir..name) then
719 return ( call(init.dir..name.." enable") == 1 )
723 --- Disable the given init script
724 -- @param name Name of the init script
725 -- @return Boolean indicating success
726 function init.disable(name)
727 if luci.fs.access(init.dir..name) then
728 return ( call(init.dir..name.." disable") == 0 )
733 -- Internal functions
735 function _parse_delimited_table(iter, delimiter)
736 delimiter = delimiter or "%s+"
739 local trim = luci.util.trim
740 local split = luci.util.split
742 local keys = split(trim(iter()), delimiter, nil, true)
743 for i, j in pairs(keys) do
744 keys[i] = trim(keys[i])
751 for i, j in pairs(split(line, delimiter, nil, true)) do
757 table.insert(data, row)
763 function _parse_mixed_record(cnt, delimiter)
764 delimiter = delimiter or " "
768 for i, l in pairs(luci.util.split(luci.util.trim(cnt), "\n")) do
769 for j, f in pairs(luci.util.split(luci.util.trim(l), delimiter, nil, true)) do
770 local k, x, v = f:match('([^%s][^:=]*) *([:=]*) *"*([^\n"]*)"*')
774 table.insert(flags, k)