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 = tonumber, ipairs, pairs, pcall
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 posix.uname("%n")
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.popen 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 cpuinfo = luci.fs.readfile("/proc/cpuinfo")
179 local meminfo = luci.fs.readfile("/proc/meminfo")
181 local system = cpuinfo:match("system typ.-:%s*([^\n]+)")
183 local memtotal = tonumber(meminfo:match("MemTotal:%s*(%d+)"))
184 local memcached = tonumber(meminfo:match("\nCached:%s*(%d+)"))
185 local memfree = tonumber(meminfo:match("MemFree:%s*(%d+)"))
186 local membuffers = tonumber(meminfo:match("Buffers:%s*(%d+)"))
189 system = posix.uname("%m")
190 model = cpuinfo:match("model name.-:%s*([^\n]+)")
192 model = cpuinfo:match("Processor.-:%s*([^\n]+)")
195 model = cpuinfo:match("cpu model.-:%s*([^\n]+)")
198 return system, model, memtotal, memcached, membuffers, memfree
201 --- Retrieves the output of the "logread" command.
202 -- @return String containing the current log buffer
204 return luci.util.exec("logread")
207 --- Retrieves the output of the "dmesg" command.
208 -- @return String containing the current log buffer
210 return luci.util.exec("dmesg")
213 --- Generates a random id with specified length.
214 -- @param bytes Number of bytes for the unique id
215 -- @return String containing hex encoded id
216 function uniqueid(bytes)
217 local fp = io.open("/dev/urandom")
218 local chunk = { fp:read(bytes):byte(1, bytes) }
223 local pattern = "%02X"
224 for i, byte in ipairs(chunk) do
225 hex = hex .. pattern:format(byte)
231 --- Returns the current system uptime stats.
232 -- @return String containing total uptime in seconds
233 -- @return String containing idle time in seconds
235 local loadavg = io.lines("/proc/uptime")()
236 return loadavg:match("^(.-) (.-)$")
239 --- LuCI system utilities / POSIX user group related functions.
241 -- @name luci.sys.group
244 --- Returns information about a POSIX user group.
247 -- @param group Group ID or name of a system user group
248 -- @return Table with information about the requested group
249 group.getgroup = posix.getgroup
252 --- LuCI system utilities / network related functions.
254 -- @name luci.sys.net
257 --- Returns the current arp-table entries as two-dimensional table.
258 -- @return Table of table containing the current arp entries.
259 -- The following fields are defined for arp entry objects:
260 -- { "IP address", "HW address", "HW type", "Flags", "Mask", "Device" }
261 function net.arptable()
262 return _parse_delimited_table(io.lines("/proc/net/arp"), "%s%s+")
265 --- Returns conntrack information
266 -- @return Table with the currently tracked IP connections
267 function net.conntrack()
269 if luci.fs.access("/proc/net/nf_conntrack", "r") then
270 for line in io.lines("/proc/net/nf_conntrack") do
271 line = line:match "^(.-( [^ =]+=).-)%2"
272 local entry, flags = _parse_mixed_record(line, " +")
273 entry.layer3 = flags[1]
274 entry.layer4 = flags[3]
279 connt[#connt+1] = entry
281 elseif luci.fs.access("/proc/net/ip_conntrack", "r") then
282 for line in io.lines("/proc/net/ip_conntrack") do
283 line = line:match "^(.-( [^ =]+=).-)%2"
284 local entry, flags = _parse_mixed_record(line, " +")
285 entry.layer3 = "ipv4"
286 entry.layer4 = flags[1]
291 connt[#connt+1] = entry
299 --- Determine the current IPv4 default route. If multiple default routes exist,
300 -- return the one with the lowest metric.
301 -- @return Table with the properties of the current default route.
302 -- The following fields are defined:
303 -- { "dest", "gateway", "metric", "refcount", "usecount", "irtt",
304 -- "flags", "device" }
305 function net.defaultroute()
307 for _, r in pairs(net.routes()) do
308 if r.dest:prefix() == 0 and (not route or route.metric > r.metric) then
315 --- Determine the current IPv6 default route. If multiple default routes exist,
316 -- return the one with the lowest metric.
317 -- @return Table with the properties of the current default route.
318 -- The following fields are defined:
319 -- { "source", "dest", "nexthop", "metric", "refcount", "usecount",
320 -- "flags", "device" }
321 function net.defaultroute6()
323 local routes6 = net.routes6()
325 for _, r in pairs(routes6) do
326 if r.dest:prefix() == 0 and
327 (not route or route.metric > r.metric)
336 --- Determine the names of available network interfaces.
337 -- @return Table containing all current interface names
338 function net.devices()
340 for line in io.lines("/proc/net/dev") do
341 table.insert(devices, line:match(" *(.-):"))
347 --- Return information about available network interfaces.
348 -- @return Table containing all current interface names and their information
349 function net.deviceinfo()
351 for line in io.lines("/proc/net/dev") do
352 local name, data = line:match("^ *(.-): *(.*)$")
353 if name and data then
354 devices[name] = luci.util.split(data, " +", nil, true)
361 -- Determine the MAC address belonging to the given IP address.
362 -- @param ip IPv4 address
363 -- @return String containing the MAC address or nil if it cannot be found
364 function net.ip4mac(ip)
367 for i, l in ipairs(net.arptable()) do
368 if l["IP address"] == ip then
369 mac = l["HW address"]
376 --- Returns the current kernel routing table entries.
377 -- @return Table of tables with properties of the corresponding routes.
378 -- The following fields are defined for route entry tables:
379 -- { "dest", "gateway", "metric", "refcount", "usecount", "irtt",
380 -- "flags", "device" }
381 function net.routes()
384 for line in io.lines("/proc/net/route") do
386 local dev, dst_ip, gateway, flags, refcnt, usecnt, metric,
387 dst_mask, mtu, win, irtt = line:match(
388 "([^%s]+)\t([A-F0-9]+)\t([A-F0-9]+)\t([A-F0-9]+)\t" ..
389 "(%d+)\t(%d+)\t(%d+)\t([A-F0-9]+)\t(%d+)\t(%d+)\t(%d+)"
393 gateway = luci.ip.Hex( gateway, 32, luci.ip.FAMILY_INET4 )
394 dst_mask = luci.ip.Hex( dst_mask, 32, luci.ip.FAMILY_INET4 )
395 dst_ip = luci.ip.Hex(
396 dst_ip, dst_mask:prefix(dst_mask), luci.ip.FAMILY_INET4
399 routes[#routes+1] = {
402 metric = tonumber(metric),
403 refcount = tonumber(refcnt),
404 usecount = tonumber(usecnt),
406 window = tonumber(window),
407 irtt = tonumber(irtt),
408 flags = tonumber(flags, 16),
417 --- Returns the current ipv6 kernel routing table entries.
418 -- @return Table of tables with properties of the corresponding routes.
419 -- The following fields are defined for route entry tables:
420 -- { "source", "dest", "nexthop", "metric", "refcount", "usecount",
421 -- "flags", "device" }
422 function net.routes6()
423 if luci.fs.access("/proc/net/ipv6_route", "r") then
426 for line in io.lines("/proc/net/ipv6_route") do
428 local dst_ip, dst_prefix, src_ip, src_prefix, nexthop,
429 metric, refcnt, usecnt, flags, dev = line:match(
430 "([a-f0-9]+) ([a-f0-9]+) " ..
431 "([a-f0-9]+) ([a-f0-9]+) " ..
432 "([a-f0-9]+) ([a-f0-9]+) " ..
433 "([a-f0-9]+) ([a-f0-9]+) " ..
434 "([a-f0-9]+) +([^%s]+)"
437 src_ip = luci.ip.Hex(
438 src_ip, tonumber(src_prefix, 16), luci.ip.FAMILY_INET6, false
441 dst_ip = luci.ip.Hex(
442 dst_ip, tonumber(dst_prefix, 16), luci.ip.FAMILY_INET6, false
445 nexthop = luci.ip.Hex( nexthop, 128, luci.ip.FAMILY_INET6, false )
447 routes[#routes+1] = {
451 metric = tonumber(metric, 16),
452 refcount = tonumber(refcnt, 16),
453 usecount = tonumber(usecnt, 16),
454 flags = tonumber(flags, 16),
463 --- Tests whether the given host responds to ping probes.
464 -- @param host String containing a hostname or IPv4 address
465 -- @return Number containing 0 on success and >= 1 on error
466 function net.pingtest(host)
467 return os.execute("ping -c1 '"..host:gsub("'", '').."' >/dev/null 2>&1")
471 --- LuCI system utilities / process related functions.
473 -- @name luci.sys.process
476 --- Get the current process id.
478 -- @name process.info
479 -- @return Number containing the current pid
480 process.info = posix.getpid
482 --- Retrieve information about currently running processes.
483 -- @return Table containing process information
484 function process.list()
487 local ps = luci.util.execi("top -bn1")
499 k = luci.util.split(luci.util.trim(line), "%s+", nil, true)
500 if k[1] == "PID" then
508 line = luci.util.trim(line)
509 for i, value in ipairs(luci.util.split(line, "%s+", #k-1, true)) do
513 local pid = tonumber(row[k[1]])
522 --- Set the gid of a process identified by given pid.
523 -- @param pid Number containing the process id
524 -- @param gid Number containing the Unix group id
525 -- @return Boolean indicating successful operation
526 -- @return String containing the error message if failed
527 -- @return Number containing the error code if failed
528 function process.setgroup(pid, gid)
529 return posix.setpid("g", pid, gid)
532 --- Set the uid of a process identified by given pid.
533 -- @param pid Number containing the process id
534 -- @param uid Number containing the Unix user 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.setuser(pid, uid)
539 return posix.setpid("u", pid, uid)
542 --- Send a signal to a process identified by given pid.
544 -- @name process.signal
545 -- @param pid Number containing the process id
546 -- @param sig Signal to send (default: 15 [SIGTERM])
547 -- @return Boolean indicating successful operation
548 -- @return Number containing the error code if failed
549 process.signal = posix.kill
552 --- LuCI system utilities / user related functions.
554 -- @name luci.sys.user
557 --- Retrieve user informations for given uid.
560 -- @param uid Number containing the Unix user id
561 -- @return Table containing the following fields:
562 -- { "uid", "gid", "name", "passwd", "dir", "shell", "gecos" }
563 user.getuser = posix.getpasswd
565 --- Test whether given string matches the password of a given system user.
566 -- @param username String containing the Unix user name
567 -- @param password String containing the password to compare
568 -- @return Boolean indicating wheather the passwords are equal
569 function user.checkpasswd(username, password)
570 local account = user.getuser(username)
573 local pwd = account.passwd
576 if luci.fs.stat("/etc/shadow") then
577 if not pcall(function()
578 for l in io.lines("/etc/shadow") do
579 shadowpw = l:match("^%s:([^:]+)" % username)
586 return nil, "Unable to access shadow-file"
595 if pwd and #pwd > 0 and password and #password > 0 then
596 return (pwd == posix.crypt(password, pwd))
603 --- Change the password of given user.
604 -- @param username String containing the Unix user name
605 -- @param password String containing the password to compare
606 -- @return Number containing 0 on success and >= 1 on error
607 function user.setpasswd(username, password)
609 password = password:gsub("'", "")
613 username = username:gsub("'", "")
616 local cmd = "(echo '"..password.."';sleep 1;echo '"..password.."')|"
617 cmd = cmd .. "passwd '"..username.."' >/dev/null 2>&1"
618 return os.execute(cmd)
622 --- LuCI system utilities / wifi related functions.
624 -- @name luci.sys.wifi
627 --- Get iwconfig output for all wireless devices.
628 -- @return Table of tables containing the iwconfing output for each wifi device
629 function wifi.getiwconfig()
630 local cnt = luci.util.exec("PATH=/sbin:/usr/sbin iwconfig 2>/dev/null")
633 for i, l in pairs(luci.util.split(luci.util.trim(cnt), "\n\n")) do
634 local k = l:match("^(.-) ")
635 l = l:gsub("^(.-) +", "", 1)
637 local entry, flags = _parse_mixed_record(l)
648 --- Get iwlist scan output from all wireless devices.
649 -- @return Table of tables contaiing all scan results
650 function wifi.iwscan(iface)
651 local siface = iface or ""
652 local cnt = luci.util.exec("iwlist "..siface.." scan 2>/dev/null")
655 for i, l in pairs(luci.util.split(luci.util.trim(cnt), "\n\n")) do
656 local k = l:match("^(.-) ")
657 l = l:gsub("^[^\n]+", "", 1)
658 l = luci.util.trim(l)
661 for j, c in pairs(luci.util.split(l, "\n Cell")) do
662 c = c:gsub("^(.-)- ", "", 1)
663 c = luci.util.split(c, "\n", 7)
664 c = table.concat(c, "\n", 1)
665 local entry, flags = _parse_mixed_record(c)
669 table.insert(iws[k], entry)
674 return iface and (iws[iface] or {}) or iws
678 --- LuCI system utilities / init related functions.
680 -- @name luci.sys.init
682 init.dir = "/etc/init.d/"
684 --- Get the names of all installed init scripts
685 -- @return Table containing the names of all inistalled init scripts
686 function init.names()
688 for _, name in ipairs(luci.fs.glob(init.dir.."*")) do
689 names[#names+1] = luci.fs.basename(name)
694 --- Test whether the given init script is enabled
695 -- @param name Name of the init script
696 -- @return Boolean indicating whether init is enabled
697 function init.enabled(name)
698 if luci.fs.access(init.dir..name) then
699 return ( call(init.dir..name.." enabled") == 0 )
704 --- Get the index of he given init script
705 -- @param name Name of the init script
706 -- @return Numeric index value
707 function init.index(name)
708 if luci.fs.access(init.dir..name) then
709 return call("source "..init.dir..name.."; exit $START")
713 --- Enable the given init script
714 -- @param name Name of the init script
715 -- @return Boolean indicating success
716 function init.enable(name)
717 if luci.fs.access(init.dir..name) then
718 return ( call(init.dir..name.." enable") == 1 )
722 --- Disable the given init script
723 -- @param name Name of the init script
724 -- @return Boolean indicating success
725 function init.disable(name)
726 if luci.fs.access(init.dir..name) then
727 return ( call(init.dir..name.." disable") == 0 )
732 -- Internal functions
734 function _parse_delimited_table(iter, delimiter)
735 delimiter = delimiter or "%s+"
738 local trim = luci.util.trim
739 local split = luci.util.split
741 local keys = split(trim(iter()), delimiter, nil, true)
742 for i, j in pairs(keys) do
743 keys[i] = trim(keys[i])
750 for i, j in pairs(split(line, delimiter, nil, true)) do
756 table.insert(data, row)
762 function _parse_mixed_record(cnt, delimiter)
763 delimiter = delimiter or " "
767 for i, l in pairs(luci.util.split(luci.util.trim(cnt), "\n")) do
768 for j, f in pairs(luci.util.split(luci.util.trim(l), delimiter, nil, true)) do
769 local k, x, v = f:match('([^%s][^:=]*) *([:=]*) *"*([^\n"]*)"*')
773 table.insert(flags, k)