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 = tonumber, ipairs, pairs
41 --- LuCI Linux and POSIX system utilities.
44 --- Execute a given shell command and return the error code
47 -- @param ... Command to call
48 -- @return Error code of the command
50 return os.execute(...) / 256
53 --- Execute a given shell command and capture its standard output
56 -- @param command Command to call
57 -- @return String containg the return the output of the command
60 --- Invoke the luci-flash executable to write an image to the flash memory.
61 -- @param image Local path or URL to image file
62 -- @param kpattern Pattern of files to keep over flash process
63 -- @return Return value of os.execute()
64 function flash(image, kpattern)
65 local cmd = "luci-flash "
67 cmd = cmd .. "-k '" .. kpattern:gsub("'", "") .. "' "
69 cmd = cmd .. "'" .. image:gsub("'", "") .. "' >/dev/null 2>&1"
71 return os.execute(cmd)
74 --- Retrieve information about currently mounted file systems.
75 -- @return Table containing mount information
78 local k = {"fs", "blocks", "used", "available", "percent", "mountpoint"}
79 local ps = luci.util.execi("df")
91 for value in line:gmatch("[^%s]+") do
98 -- this is a rather ugly workaround to cope with wrapped lines in
101 -- /dev/scsi/host0/bus0/target0/lun0/part3
102 -- 114382024 93566472 15005244 86% /mnt/usb
105 if not row[k[2]] then
108 for value in line:gmatch("[^%s]+") do
114 table.insert(data, row)
121 --- Retrieve environment variables. If no variable is given then a table
122 -- containing the whole environment is returned otherwise this function returns
123 -- the corresponding string value for the given name or nil if no such variable
127 -- @param var Name of the environment variable to retrieve (optional)
128 -- @return String containg the value of the specified variable
129 -- @return Table containing all variables if no variable name is given
130 getenv = posix.getenv
132 --- Determine the current hostname.
133 -- @return String containing the system hostname
135 return io.lines("/proc/sys/kernel/hostname")()
138 --- Returns the contents of a documented referred by an URL.
139 -- @param url The URL to retrieve
140 -- @param stream Return a stream instead of a buffer
141 -- @param target Directly write to target file name
142 -- @return String containing the contents of given the URL
143 function httpget(url, stream, target)
145 local source = stream and io.open or luci.util.exec
146 return source("wget -qO- '"..url:gsub("'", "").."'")
148 return os.execute("wget -qO '%s' '%s'" %
149 {target:gsub("'", ""), url:gsub("'", "")})
153 --- Returns the system load average values.
154 -- @return String containing the average load value 1 minute ago
155 -- @return String containing the average load value 5 minutes ago
156 -- @return String containing the average load value 15 minutes ago
157 -- @return String containing the active and total number of processes
158 -- @return String containing the last used pid
160 local loadavg = io.lines("/proc/loadavg")()
161 return loadavg:match("^(.-) (.-) (.-) (.-) (.-)$")
164 --- Initiate a system reboot.
165 -- @return Return value of os.execute()
167 return os.execute("reboot >/dev/null 2>&1")
170 --- Returns the system type, cpu name and installed physical memory.
171 -- @return String containing the system or platform identifier
172 -- @return String containing hardware model information
173 -- @return String containing the total memory amount in kB
174 -- @return String containing the memory used for caching in kB
175 -- @return String containing the memory used for buffering in kB
176 -- @return String containing the free memory amount in kB
178 local c1 = "cat /proc/cpuinfo|grep system\\ typ|cut -d: -f2 2>/dev/null"
179 local c2 = "uname -m 2>/dev/null"
180 local c3 = "cat /proc/cpuinfo|grep model\\ name|cut -d: -f2 2>/dev/null"
181 local c4 = "cat /proc/cpuinfo|grep cpu\\ model|cut -d: -f2 2>/dev/null"
182 local c5 = "cat /proc/meminfo|grep MemTotal|awk {' print $2 '} 2>/dev/null"
183 local c6 = "cat /proc/meminfo|grep ^Cached|awk {' print $2 '} 2>/dev/null"
184 local c7 = "cat /proc/meminfo|grep MemFree|awk {' print $2 '} 2>/dev/null"
185 local c8 = "cat /proc/meminfo|grep Buffers|awk {' print $2 '} 2>/dev/null"
187 local system = luci.util.trim(luci.util.exec(c1))
189 local memtotal = tonumber(luci.util.trim(luci.util.exec(c5)))
190 local memcached = tonumber(luci.util.trim(luci.util.exec(c6)))
191 local memfree = tonumber(luci.util.trim(luci.util.exec(c7)))
192 local membuffers = tonumber(luci.util.trim(luci.util.exec(c8)))
195 system = luci.util.trim(luci.util.exec(c2))
196 model = luci.util.trim(luci.util.exec(c3))
198 model = luci.util.trim(luci.util.exec(c4))
201 return system, model, memtotal, memcached, membuffers, memfree
204 --- Retrieves the output of the "logread" command.
205 -- @return String containing the current log buffer
207 return luci.util.exec("logread")
210 --- Generates a random id with specified length.
211 -- @param bytes Number of bytes for the unique id
212 -- @return String containing hex encoded id
213 function uniqueid(bytes)
214 local fp = io.open("/dev/urandom")
215 local chunk = { fp:read(bytes):byte(1, bytes) }
220 local pattern = "%02X"
221 for i, byte in ipairs(chunk) do
222 hex = hex .. pattern:format(byte)
228 --- Returns the current system uptime stats.
229 -- @return String containing total uptime in seconds
230 -- @return String containing idle time in seconds
232 local loadavg = io.lines("/proc/uptime")()
233 return loadavg:match("^(.-) (.-)$")
236 --- LuCI system utilities / POSIX user group related functions.
238 -- @name luci.sys.group
241 --- Returns information about a POSIX user group.
244 -- @param group Group ID or name of a system user group
245 -- @return Table with information about the requested group
246 group.getgroup = posix.getgroup
249 --- LuCI system utilities / network related functions.
251 -- @name luci.sys.net
254 --- Returns the current arp-table entries as two-dimensional table.
255 -- @return Table of table containing the current arp entries.
256 -- The following fields are defined for arp entry objects:
257 -- { "IP address", "HW address", "HW type", "Flags", "Mask", "Device" }
258 function net.arptable()
259 return _parse_delimited_table(io.lines("/proc/net/arp"), "%s%s+")
262 --- Determine the current default route.
263 -- @return Table with the properties of the current default route.
264 -- The following fields are defined:
265 -- { "Mask", "RefCnt", "Iface", "Flags", "Window", "IRTT",
266 -- "MTU", "Gateway", "Destination", "Metric", "Use" }
267 function net.defaultroute()
268 local routes = net.routes()
271 for i, r in pairs(luci.sys.net.routes()) do
272 if r.Destination == "00000000" and (not route or route.Metric > r.Metric) then
280 --- Determine the names of available network interfaces.
281 -- @return Table containing all current interface names
282 function net.devices()
284 for line in io.lines("/proc/net/dev") do
285 table.insert(devices, line:match(" *(.-):"))
291 --- Return information about available network interfaces.
292 -- @return Table containing all current interface names and their information
293 function net.deviceinfo()
295 for line in io.lines("/proc/net/dev") do
296 local name, data = line:match("^ *(.-): *(.*)$")
297 if name and data then
298 devices[name] = luci.util.split(data, " +", nil, true)
305 -- Determine the MAC address belonging to the given IP address.
306 -- @param ip IPv4 address
307 -- @return String containing the MAC address or nil if it cannot be found
308 function net.ip4mac(ip)
311 for i, l in ipairs(net.arptable()) do
312 if l["IP address"] == ip then
313 mac = l["HW address"]
320 --- Returns the current kernel routing table entries.
321 -- @return Table of tables with properties of the corresponding routes.
322 -- The following fields are defined for route entry tables:
323 -- { "Mask", "RefCnt", "Iface", "Flags", "Window", "IRTT",
324 -- "MTU", "Gateway", "Destination", "Metric", "Use" }
325 function net.routes()
326 return _parse_delimited_table(io.lines("/proc/net/route"))
330 --- Tests whether the given host responds to ping probes.
331 -- @param host String containing a hostname or IPv4 address
332 -- @return Number containing 0 on success and >= 1 on error
333 function net.pingtest(host)
334 return os.execute("ping -c1 '"..host:gsub("'", '').."' >/dev/null 2>&1")
338 --- LuCI system utilities / process related functions.
340 -- @name luci.sys.process
343 --- Get the current process id.
345 -- @name process.info
346 -- @return Number containing the current pid
347 process.info = posix.getpid
349 --- Retrieve information about currently running processes.
350 -- @return Table containing process information
351 function process.list()
354 local ps = luci.util.execi("top -bn1")
366 k = luci.util.split(luci.util.trim(line), "%s+", nil, true)
367 if k[1] == "PID" then
375 line = luci.util.trim(line)
376 for i, value in ipairs(luci.util.split(line, "%s+", #k-1, true)) do
380 local pid = tonumber(row[k[1]])
389 --- Set the gid of a process identified by given pid.
390 -- @param pid Number containing the process id
391 -- @param gid Number containing the Unix group id
392 -- @return Boolean indicating successful operation
393 -- @return String containing the error message if failed
394 -- @return Number containing the error code if failed
395 function process.setgroup(pid, gid)
396 return posix.setpid("g", pid, gid)
399 --- Set the uid of a process identified by given pid.
400 -- @param pid Number containing the process id
401 -- @param uid Number containing the Unix user id
402 -- @return Boolean indicating successful operation
403 -- @return String containing the error message if failed
404 -- @return Number containing the error code if failed
405 function process.setuser(pid, uid)
406 return posix.setpid("u", pid, uid)
409 --- Send a signal to a process identified by given pid.
411 -- @name process.signal
412 -- @param pid Number containing the process id
413 -- @param sig Signal to send (default: 15 [SIGTERM])
414 -- @return Boolean indicating successful operation
415 -- @return Number containing the error code if failed
416 process.signal = posix.kill
419 --- LuCI system utilities / user related functions.
421 -- @name luci.sys.user
424 --- Retrieve user informations for given uid.
427 -- @param uid Number containing the Unix user id
428 -- @return Table containing the following fields:
429 -- { "uid", "gid", "name", "passwd", "dir", "shell", "gecos" }
430 user.getuser = posix.getpasswd
432 --- Test whether given string matches the password of a given system user.
433 -- @param username String containing the Unix user name
434 -- @param password String containing the password to compare
435 -- @return Boolean indicating wheather the passwords are equal
436 function user.checkpasswd(username, password)
437 local account = user.getuser(username)
440 local pwd = account.passwd
443 if luci.fs.stat("/etc/shadow") then
444 if not pcall(function()
445 for l in io.lines("/etc/shadow") do
446 shadowpw = l:match("^%s:([^:]+)" % username)
453 return nil, "Unable to access shadow-file"
462 return (pwd == posix.crypt(password, pwd))
466 --- Change the password of given user.
467 -- @param username String containing the Unix user name
468 -- @param password String containing the password to compare
469 -- @return Number containing 0 on success and >= 1 on error
470 function user.setpasswd(username, password)
472 password = password:gsub("'", "")
476 username = username:gsub("'", "")
479 local cmd = "(echo '"..password.."';sleep 1;echo '"..password.."')|"
480 cmd = cmd .. "passwd '"..username.."' >/dev/null 2>&1"
481 return os.execute(cmd)
485 --- LuCI system utilities / wifi related functions.
487 -- @name luci.sys.wifi
490 --- Get iwconfig output for all wireless devices.
491 -- @return Table of tables containing the iwconfing output for each wifi device
492 function wifi.getiwconfig()
493 local cnt = luci.util.exec("/usr/sbin/iwconfig 2>/dev/null")
496 for i, l in pairs(luci.util.split(luci.util.trim(cnt), "\n\n")) do
497 local k = l:match("^(.-) ")
498 l = l:gsub("^(.-) +", "", 1)
500 iwc[k] = _parse_mixed_record(l)
507 --- Get iwlist scan output from all wireless devices.
508 -- @return Table of tables contaiing all scan results
509 function wifi.iwscan(iface)
510 local siface = iface or ""
511 local cnt = luci.util.exec("iwlist "..siface.." scan 2>/dev/null")
514 for i, l in pairs(luci.util.split(luci.util.trim(cnt), "\n\n")) do
515 local k = l:match("^(.-) ")
516 l = l:gsub("^[^\n]+", "", 1)
517 l = luci.util.trim(l)
520 for j, c in pairs(luci.util.split(l, "\n Cell")) do
521 c = c:gsub("^(.-)- ", "", 1)
522 c = luci.util.split(c, "\n", 7)
523 c = table.concat(c, "\n", 1)
524 table.insert(iws[k], _parse_mixed_record(c))
529 return iface and (iws[iface] or {}) or iws
533 --- LuCI system utilities / init related functions.
535 -- @name luci.sys.init
537 init.dir = "/etc/init.d/"
539 --- Get the names of all installed init scripts
540 -- @return Table containing the names of all inistalled init scripts
541 function init.names()
543 for _, name in ipairs(luci.fs.glob(init.dir.."*")) do
544 names[#names+1] = luci.fs.basename(name)
549 --- Test whether the given init script is enabled
550 -- @param name Name of the init script
551 -- @return Boolean indicating whether init is enabled
552 function init.enabled(name)
553 if luci.fs.access(init.dir..name) then
554 return ( call(init.dir..name.." enabled") == 0 )
559 --- Get the index of he given init script
560 -- @param name Name of the init script
561 -- @return Numeric index value
562 function init.index(name)
563 if luci.fs.access(init.dir..name) then
564 return call("source "..init.dir..name.."; exit $START")
568 --- Enable the given init script
569 -- @param name Name of the init script
570 -- @return Boolean indicating success
571 function init.enable(name)
572 if luci.fs.access(init.dir..name) then
573 return ( call(init.dir..name.." enable") == 1 )
577 --- Disable the given init script
578 -- @param name Name of the init script
579 -- @return Boolean indicating success
580 function init.disable(name)
581 if luci.fs.access(init.dir..name) then
582 return ( call(init.dir..name.." disable") == 0 )
587 -- Internal functions
589 function _parse_delimited_table(iter, delimiter)
590 delimiter = delimiter or "%s+"
593 local trim = luci.util.trim
594 local split = luci.util.split
596 local keys = split(trim(iter()), delimiter, nil, true)
597 for i, j in pairs(keys) do
598 keys[i] = trim(keys[i])
605 for i, j in pairs(split(line, delimiter, nil, true)) do
611 table.insert(data, row)
617 function _parse_mixed_record(cnt)
620 for i, l in pairs(luci.util.split(luci.util.trim(cnt), "\n")) do
621 for j, f in pairs(luci.util.split(luci.util.trim(l), " ")) do
622 local k, x, v = f:match('([^%s][^:=]+) *([:=]*) *"*([^\n"]*)"*')
626 table.insert(data, k)