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 posix = require "posix"
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 = posix.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 posix.uname("%n")
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
164 -- @return String containing the active and total number of processes
165 -- @return String containing the last used pid
167 local loadavg = io.lines("/proc/loadavg")()
168 return loadavg:match("^(.-) (.-) (.-) (.-) (.-)$")
171 --- Initiate a system reboot.
172 -- @return Return value of os.execute()
174 return os.execute("reboot >/dev/null 2>&1")
177 --- Returns the system type, cpu name and installed physical memory.
178 -- @return String containing the system or platform identifier
179 -- @return String containing hardware model information
180 -- @return String containing the total memory amount in kB
181 -- @return String containing the memory used for caching in kB
182 -- @return String containing the memory used for buffering in kB
183 -- @return String containing the free memory amount in kB
185 local cpuinfo = luci.fs.readfile("/proc/cpuinfo")
186 local meminfo = luci.fs.readfile("/proc/meminfo")
188 local system = cpuinfo:match("system typ.-:%s*([^\n]+)")
190 local memtotal = tonumber(meminfo:match("MemTotal:%s*(%d+)"))
191 local memcached = tonumber(meminfo:match("\nCached:%s*(%d+)"))
192 local memfree = tonumber(meminfo:match("MemFree:%s*(%d+)"))
193 local membuffers = tonumber(meminfo:match("Buffers:%s*(%d+)"))
196 system = posix.uname("%m")
197 model = cpuinfo:match("model name.-:%s*([^\n]+)")
199 model = cpuinfo:match("Processor.-:%s*([^\n]+)")
202 model = cpuinfo:match("cpu model.-:%s*([^\n]+)")
205 return system, model, memtotal, memcached, membuffers, memfree
208 --- Retrieves the output of the "logread" command.
209 -- @return String containing the current log buffer
211 return luci.util.exec("logread")
214 --- Retrieves the output of the "dmesg" command.
215 -- @return String containing the current log buffer
217 return luci.util.exec("dmesg")
220 --- Generates a random id with specified length.
221 -- @param bytes Number of bytes for the unique id
222 -- @return String containing hex encoded id
223 function uniqueid(bytes)
224 local fp = io.open("/dev/urandom")
225 local chunk = { fp:read(bytes):byte(1, bytes) }
230 local pattern = "%02X"
231 for i, byte in ipairs(chunk) do
232 hex = hex .. pattern:format(byte)
238 --- Returns the current system uptime stats.
239 -- @return String containing total uptime in seconds
240 -- @return String containing idle time in seconds
242 local loadavg = io.lines("/proc/uptime")()
243 return loadavg:match("^(.-) (.-)$")
246 --- LuCI system utilities / POSIX user group related functions.
248 -- @name luci.sys.group
251 --- Returns information about a POSIX user group.
254 -- @param group Group ID or name of a system user group
255 -- @return Table with information about the requested group
256 group.getgroup = posix.getgroup
259 --- LuCI system utilities / network related functions.
261 -- @name luci.sys.net
264 --- Returns the current arp-table entries as two-dimensional table.
265 -- @return Table of table containing the current arp entries.
266 -- The following fields are defined for arp entry objects:
267 -- { "IP address", "HW address", "HW type", "Flags", "Mask", "Device" }
268 function net.arptable()
269 return _parse_delimited_table(io.lines("/proc/net/arp"), "%s%s+")
272 --- Returns conntrack information
273 -- @return Table with the currently tracked IP connections
274 function net.conntrack()
276 if luci.fs.access("/proc/net/nf_conntrack", "r") then
277 for line in io.lines("/proc/net/nf_conntrack") do
278 line = line:match "^(.-( [^ =]+=).-)%2"
279 local entry, flags = _parse_mixed_record(line, " +")
280 entry.layer3 = flags[1]
281 entry.layer4 = flags[3]
286 connt[#connt+1] = entry
288 elseif luci.fs.access("/proc/net/ip_conntrack", "r") then
289 for line in io.lines("/proc/net/ip_conntrack") do
290 line = line:match "^(.-( [^ =]+=).-)%2"
291 local entry, flags = _parse_mixed_record(line, " +")
292 entry.layer3 = "ipv4"
293 entry.layer4 = flags[1]
298 connt[#connt+1] = entry
306 --- Determine the current IPv4 default route. If multiple default routes exist,
307 -- return the one with the lowest metric.
308 -- @return Table with the properties of the current default route.
309 -- The following fields are defined:
310 -- { "dest", "gateway", "metric", "refcount", "usecount", "irtt",
311 -- "flags", "device" }
312 function net.defaultroute()
314 for _, r in pairs(net.routes()) do
315 if r.dest:prefix() == 0 and (not route or route.metric > r.metric) then
322 --- Determine the current IPv6 default route. If multiple default routes exist,
323 -- return the one with the lowest metric.
324 -- @return Table with the properties of the current default route.
325 -- The following fields are defined:
326 -- { "source", "dest", "nexthop", "metric", "refcount", "usecount",
327 -- "flags", "device" }
328 function net.defaultroute6()
330 local routes6 = net.routes6()
332 for _, r in pairs(routes6) do
333 if r.dest:prefix() == 0 and
334 (not route or route.metric > r.metric)
343 --- Determine the names of available network interfaces.
344 -- @return Table containing all current interface names
345 function net.devices()
347 for line in io.lines("/proc/net/dev") do
348 table.insert(devices, line:match(" *(.-):"))
354 --- Return information about available network interfaces.
355 -- @return Table containing all current interface names and their information
356 function net.deviceinfo()
358 for line in io.lines("/proc/net/dev") do
359 local name, data = line:match("^ *(.-): *(.*)$")
360 if name and data then
361 devices[name] = luci.util.split(data, " +", nil, true)
368 -- Determine the MAC address belonging to the given IP address.
369 -- @param ip IPv4 address
370 -- @return String containing the MAC address or nil if it cannot be found
371 function net.ip4mac(ip)
374 for i, l in ipairs(net.arptable()) do
375 if l["IP address"] == ip then
376 mac = l["HW address"]
383 --- Returns the current kernel routing table entries.
384 -- @return Table of tables with properties of the corresponding routes.
385 -- The following fields are defined for route entry tables:
386 -- { "dest", "gateway", "metric", "refcount", "usecount", "irtt",
387 -- "flags", "device" }
388 function net.routes()
391 for line in io.lines("/proc/net/route") do
393 local dev, dst_ip, gateway, flags, refcnt, usecnt, metric,
394 dst_mask, mtu, win, irtt = line:match(
395 "([^%s]+)\t([A-F0-9]+)\t([A-F0-9]+)\t([A-F0-9]+)\t" ..
396 "(%d+)\t(%d+)\t(%d+)\t([A-F0-9]+)\t(%d+)\t(%d+)\t(%d+)"
400 gateway = luci.ip.Hex( gateway, 32, luci.ip.FAMILY_INET4 )
401 dst_mask = luci.ip.Hex( dst_mask, 32, luci.ip.FAMILY_INET4 )
402 dst_ip = luci.ip.Hex(
403 dst_ip, dst_mask:prefix(dst_mask), luci.ip.FAMILY_INET4
406 routes[#routes+1] = {
409 metric = tonumber(metric),
410 refcount = tonumber(refcnt),
411 usecount = tonumber(usecnt),
413 window = tonumber(window),
414 irtt = tonumber(irtt),
415 flags = tonumber(flags, 16),
424 --- Returns the current ipv6 kernel routing table entries.
425 -- @return Table of tables with properties of the corresponding routes.
426 -- The following fields are defined for route entry tables:
427 -- { "source", "dest", "nexthop", "metric", "refcount", "usecount",
428 -- "flags", "device" }
429 function net.routes6()
430 if luci.fs.access("/proc/net/ipv6_route", "r") then
433 for line in io.lines("/proc/net/ipv6_route") do
435 local dst_ip, dst_prefix, src_ip, src_prefix, nexthop,
436 metric, refcnt, usecnt, flags, dev = line:match(
437 "([a-f0-9]+) ([a-f0-9]+) " ..
438 "([a-f0-9]+) ([a-f0-9]+) " ..
439 "([a-f0-9]+) ([a-f0-9]+) " ..
440 "([a-f0-9]+) ([a-f0-9]+) " ..
441 "([a-f0-9]+) +([^%s]+)"
444 src_ip = luci.ip.Hex(
445 src_ip, tonumber(src_prefix, 16), luci.ip.FAMILY_INET6, false
448 dst_ip = luci.ip.Hex(
449 dst_ip, tonumber(dst_prefix, 16), luci.ip.FAMILY_INET6, false
452 nexthop = luci.ip.Hex( nexthop, 128, luci.ip.FAMILY_INET6, false )
454 routes[#routes+1] = {
458 metric = tonumber(metric, 16),
459 refcount = tonumber(refcnt, 16),
460 usecount = tonumber(usecnt, 16),
461 flags = tonumber(flags, 16),
470 --- Tests whether the given host responds to ping probes.
471 -- @param host String containing a hostname or IPv4 address
472 -- @return Number containing 0 on success and >= 1 on error
473 function net.pingtest(host)
474 return os.execute("ping -c1 '"..host:gsub("'", '').."' >/dev/null 2>&1")
478 --- LuCI system utilities / process related functions.
480 -- @name luci.sys.process
483 --- Get the current process id.
485 -- @name process.info
486 -- @return Number containing the current pid
487 process.info = posix.getpid
489 --- Retrieve information about currently running processes.
490 -- @return Table containing process information
491 function process.list()
494 local ps = luci.util.execi("top -bn1")
506 k = luci.util.split(luci.util.trim(line), "%s+", nil, true)
507 if k[1] == "PID" then
515 line = luci.util.trim(line)
516 for i, value in ipairs(luci.util.split(line, "%s+", #k-1, true)) do
520 local pid = tonumber(row[k[1]])
529 --- Set the gid of a process identified by given pid.
530 -- @param pid Number containing the process id
531 -- @param gid Number containing the Unix group id
532 -- @return Boolean indicating successful operation
533 -- @return String containing the error message if failed
534 -- @return Number containing the error code if failed
535 function process.setgroup(pid, gid)
536 return posix.setpid("g", pid, gid)
539 --- Set the uid of a process identified by given pid.
540 -- @param pid Number containing the process id
541 -- @param uid Number containing the Unix user id
542 -- @return Boolean indicating successful operation
543 -- @return String containing the error message if failed
544 -- @return Number containing the error code if failed
545 function process.setuser(pid, uid)
546 return posix.setpid("u", pid, uid)
549 --- Send a signal to a process identified by given pid.
551 -- @name process.signal
552 -- @param pid Number containing the process id
553 -- @param sig Signal to send (default: 15 [SIGTERM])
554 -- @return Boolean indicating successful operation
555 -- @return Number containing the error code if failed
556 process.signal = posix.kill
559 --- LuCI system utilities / user related functions.
561 -- @name luci.sys.user
564 --- Retrieve user informations for given uid.
567 -- @param uid Number containing the Unix user id
568 -- @return Table containing the following fields:
569 -- { "uid", "gid", "name", "passwd", "dir", "shell", "gecos" }
570 user.getuser = posix.getpasswd
572 --- Test whether given string matches the password of a given system user.
573 -- @param username String containing the Unix user name
574 -- @param password String containing the password to compare
575 -- @return Boolean indicating wheather the passwords are equal
576 function user.checkpasswd(username, password)
577 local account = user.getuser(username)
580 local pwd = account.passwd
583 if luci.fs.stat("/etc/shadow") then
584 if not pcall(function()
585 for l in io.lines("/etc/shadow") do
586 shadowpw = l:match("^%s:([^:]+)" % username)
593 return nil, "Unable to access shadow-file"
602 if pwd and #pwd > 0 and password and #password > 0 then
603 return (pwd == posix.crypt(password, pwd))
610 --- Change the password of given user.
611 -- @param username String containing the Unix user name
612 -- @param password String containing the password to compare
613 -- @return Number containing 0 on success and >= 1 on error
614 function user.setpasswd(username, password)
616 password = password:gsub("'", "")
620 username = username:gsub("'", "")
623 local cmd = "(echo '"..password.."';sleep 1;echo '"..password.."')|"
624 cmd = cmd .. "passwd '"..username.."' >/dev/null 2>&1"
625 return os.execute(cmd)
629 --- LuCI system utilities / wifi related functions.
631 -- @name luci.sys.wifi
634 --- Get iwconfig output for all wireless devices.
635 -- @return Table of tables containing the iwconfing output for each wifi device
636 function wifi.getiwconfig()
637 local cnt = luci.util.exec("PATH=/sbin:/usr/sbin iwconfig 2>/dev/null")
640 for i, l in pairs(luci.util.split(luci.util.trim(cnt), "\n\n")) do
641 local k = l:match("^(.-) ")
642 l = l:gsub("^(.-) +", "", 1)
644 local entry, flags = _parse_mixed_record(l)
655 --- Get iwlist scan output from all wireless devices.
656 -- @return Table of tables contaiing all scan results
657 function wifi.iwscan(iface)
658 local siface = iface or ""
659 local cnt = luci.util.exec("iwlist "..siface.." scan 2>/dev/null")
662 for i, l in pairs(luci.util.split(luci.util.trim(cnt), "\n\n")) do
663 local k = l:match("^(.-) ")
664 l = l:gsub("^[^\n]+", "", 1)
665 l = luci.util.trim(l)
668 for j, c in pairs(luci.util.split(l, "\n Cell")) do
669 c = c:gsub("^(.-)- ", "", 1)
670 c = luci.util.split(c, "\n", 7)
671 c = table.concat(c, "\n", 1)
672 local entry, flags = _parse_mixed_record(c)
676 table.insert(iws[k], entry)
681 return iface and (iws[iface] or {}) or iws
684 --- Get available channels from given wireless iface.
685 -- @param iface Wireless interface (optional)
686 -- @return Table of available channels
687 function wifi.channels(iface)
688 local cmd = "iwlist " .. ( iface or "" ) .. " freq 2>/dev/null"
691 local fd = io.popen(cmd)
695 ln = fd:read("*l") or ""
696 c, f = ln:match("Channel (%d+) : (%d+%.%d+) GHz")
698 cns[tonumber(c)] = tonumber(f)
700 until not ( #ln > 0 )
704 if not ((pairs(cns))(cns)) then
706 2.412, 2.417, 2.422, 2.427, 2.432, 2.437,
707 2.442, 2.447, 2.452, 2.457, 2.462
715 --- LuCI system utilities / init related functions.
717 -- @name luci.sys.init
719 init.dir = "/etc/init.d/"
721 --- Get the names of all installed init scripts
722 -- @return Table containing the names of all inistalled init scripts
723 function init.names()
725 for _, name in ipairs(luci.fs.glob(init.dir.."*")) do
726 names[#names+1] = luci.fs.basename(name)
731 --- Test whether the given init script is enabled
732 -- @param name Name of the init script
733 -- @return Boolean indicating whether init is enabled
734 function init.enabled(name)
735 if luci.fs.access(init.dir..name) then
736 return ( call(init.dir..name.." enabled") == 0 )
741 --- Get the index of he given init script
742 -- @param name Name of the init script
743 -- @return Numeric index value
744 function init.index(name)
745 if luci.fs.access(init.dir..name) then
746 return call("source "..init.dir..name.."; exit $START")
750 --- Enable the given init script
751 -- @param name Name of the init script
752 -- @return Boolean indicating success
753 function init.enable(name)
754 if luci.fs.access(init.dir..name) then
755 return ( call(init.dir..name.." enable") == 1 )
759 --- Disable the given init script
760 -- @param name Name of the init script
761 -- @return Boolean indicating success
762 function init.disable(name)
763 if luci.fs.access(init.dir..name) then
764 return ( call(init.dir..name.." disable") == 0 )
769 -- Internal functions
771 function _parse_delimited_table(iter, delimiter)
772 delimiter = delimiter or "%s+"
775 local trim = luci.util.trim
776 local split = luci.util.split
778 local keys = split(trim(iter()), delimiter, nil, true)
779 for i, j in pairs(keys) do
780 keys[i] = trim(keys[i])
787 for i, j in pairs(split(line, delimiter, nil, true)) do
793 table.insert(data, row)
799 function _parse_mixed_record(cnt, delimiter)
800 delimiter = delimiter or " "
804 for i, l in pairs(luci.util.split(luci.util.trim(cnt), "\n")) do
805 for j, f in pairs(luci.util.split(luci.util.trim(l), delimiter, nil, true)) do
806 local k, x, v = f:match('([^%s][^:=]*) *([:=]*) *"*([^\n"]*)"*')
810 table.insert(flags, k)