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()
301 net.routes(function(rt)
302 if rt.dest:prefix() == 0 and (not route or route.metric > rt.metric) then
310 --- Determine the current IPv6 default route. If multiple default routes exist,
311 -- return the one with the lowest metric.
312 -- @return Table with the properties of the current default route.
313 -- The following fields are defined:
314 -- { "source", "dest", "nexthop", "metric", "refcount", "usecount",
315 -- "flags", "device" }
316 function net.defaultroute6()
318 local routes6 = net.routes6()
320 for _, r in pairs(routes6) do
321 if r.dest:prefix() == 0 and
322 (not route or route.metric > r.metric)
331 --- Determine the names of available network interfaces.
332 -- @return Table containing all current interface names
333 function net.devices()
335 for line in io.lines("/proc/net/dev") do
336 table.insert(devices, line:match(" *(.-):"))
342 --- Return information about available network interfaces.
343 -- @return Table containing all current interface names and their information
344 function net.deviceinfo()
346 for line in io.lines("/proc/net/dev") do
347 local name, data = line:match("^ *(.-): *(.*)$")
348 if name and data then
349 devices[name] = luci.util.split(data, " +", nil, true)
356 -- Determine the MAC address belonging to the given IP address.
357 -- @param ip IPv4 address
358 -- @return String containing the MAC address or nil if it cannot be found
359 function net.ip4mac(ip)
362 for i, l in ipairs(net.arptable()) do
363 if l["IP address"] == ip then
364 mac = l["HW address"]
371 --- Returns the current kernel routing table entries.
372 -- @return Table of tables with properties of the corresponding routes.
373 -- The following fields are defined for route entry tables:
374 -- { "dest", "gateway", "metric", "refcount", "usecount", "irtt",
375 -- "flags", "device" }
376 function net.routes(callback)
379 for line in io.lines("/proc/net/route") do
381 local dev, dst_ip, gateway, flags, refcnt, usecnt, metric,
382 dst_mask, mtu, win, irtt = line:match(
383 "([^%s]+)\t([A-F0-9]+)\t([A-F0-9]+)\t([A-F0-9]+)\t" ..
384 "(%d+)\t(%d+)\t(%d+)\t([A-F0-9]+)\t(%d+)\t(%d+)\t(%d+)"
388 gateway = luci.ip.Hex( gateway, 32, luci.ip.FAMILY_INET4 )
389 dst_mask = luci.ip.Hex( dst_mask, 32, luci.ip.FAMILY_INET4 )
390 dst_ip = luci.ip.Hex(
391 dst_ip, dst_mask:prefix(dst_mask), luci.ip.FAMILY_INET4
397 metric = tonumber(metric),
398 refcount = tonumber(refcnt),
399 usecount = tonumber(usecnt),
401 window = tonumber(window),
402 irtt = tonumber(irtt),
403 flags = tonumber(flags, 16),
410 routes[#routes+1] = rt
418 --- Returns the current ipv6 kernel routing table entries.
419 -- @return Table of tables with properties of the corresponding routes.
420 -- The following fields are defined for route entry tables:
421 -- { "source", "dest", "nexthop", "metric", "refcount", "usecount",
422 -- "flags", "device" }
423 function net.routes6()
424 if luci.fs.access("/proc/net/ipv6_route", "r") then
427 for line in io.lines("/proc/net/ipv6_route") do
429 local dst_ip, dst_prefix, src_ip, src_prefix, nexthop,
430 metric, refcnt, usecnt, flags, dev = line:match(
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]+) ([a-f0-9]+) " ..
435 "([a-f0-9]+) +([^%s]+)"
438 src_ip = luci.ip.Hex(
439 src_ip, tonumber(src_prefix, 16), luci.ip.FAMILY_INET6, false
442 dst_ip = luci.ip.Hex(
443 dst_ip, tonumber(dst_prefix, 16), luci.ip.FAMILY_INET6, false
446 nexthop = luci.ip.Hex( nexthop, 128, luci.ip.FAMILY_INET6, false )
448 routes[#routes+1] = {
452 metric = tonumber(metric, 16),
453 refcount = tonumber(refcnt, 16),
454 usecount = tonumber(usecnt, 16),
455 flags = tonumber(flags, 16),
464 --- Tests whether the given host responds to ping probes.
465 -- @param host String containing a hostname or IPv4 address
466 -- @return Number containing 0 on success and >= 1 on error
467 function net.pingtest(host)
468 return os.execute("ping -c1 '"..host:gsub("'", '').."' >/dev/null 2>&1")
472 --- LuCI system utilities / process related functions.
474 -- @name luci.sys.process
477 --- Get the current process id.
479 -- @name process.info
480 -- @return Number containing the current pid
481 function process.info(key)
482 local s = {uid = nixio.getuid(), gid = nixio.getgid()}
483 return not key and s or s[key]
486 --- Retrieve information about currently running processes.
487 -- @return Table containing process information
488 function process.list()
491 local ps = luci.util.execi("top -bn1")
503 k = luci.util.split(luci.util.trim(line), "%s+", nil, true)
504 if k[1] == "PID" then
512 line = luci.util.trim(line)
513 for i, value in ipairs(luci.util.split(line, "%s+", #k-1, true)) do
517 local pid = tonumber(row[k[1]])
526 --- Set the gid of a process identified by given pid.
527 -- @param gid Number containing the Unix group id
528 -- @return Boolean indicating successful operation
529 -- @return String containing the error message if failed
530 -- @return Number containing the error code if failed
531 function process.setgroup(gid)
532 return nixio.setgid(gid)
535 --- Set the uid of a process identified by given pid.
536 -- @param uid Number containing the Unix user id
537 -- @return Boolean indicating successful operation
538 -- @return String containing the error message if failed
539 -- @return Number containing the error code if failed
540 function process.setuser(uid)
541 return nixio.setuid(uid)
544 --- Send a signal to a process identified by given pid.
546 -- @name process.signal
547 -- @param pid Number containing the process id
548 -- @param sig Signal to send (default: 15 [SIGTERM])
549 -- @return Boolean indicating successful operation
550 -- @return Number containing the error code if failed
551 process.signal = nixio.kill
554 --- LuCI system utilities / user related functions.
556 -- @name luci.sys.user
559 --- Retrieve user informations for given uid.
562 -- @param uid Number containing the Unix user id
563 -- @return Table containing the following fields:
564 -- { "uid", "gid", "name", "passwd", "dir", "shell", "gecos" }
565 user.getuser = nixio.getpw
567 --- Test whether given string matches the password of a given system user.
568 -- @param username String containing the Unix user name
569 -- @param pass String containing the password to compare
570 -- @return Boolean indicating wheather the passwords are equal
571 function user.checkpasswd(username, pass)
572 local pwe = nixio.getsp and nixio.getsp(username) or nixio.getpw(username)
573 local pwh = pwe and (pwe.pwdp or pwe.passwd)
574 if not pwh or #pwh < 1 or pwh ~= "!" and nixio.crypt(pass, pwh) ~= pwh then
581 --- Change the password of given user.
582 -- @param username String containing the Unix user name
583 -- @param password String containing the password to compare
584 -- @return Number containing 0 on success and >= 1 on error
585 function user.setpasswd(username, password)
587 password = password:gsub("'", "")
591 username = username:gsub("'", "")
594 local cmd = "(echo '"..password.."';sleep 1;echo '"..password.."')|"
595 cmd = cmd .. "passwd '"..username.."' >/dev/null 2>&1"
596 return os.execute(cmd)
600 --- LuCI system utilities / wifi related functions.
602 -- @name luci.sys.wifi
605 --- Get iwconfig output for all wireless devices.
606 -- @return Table of tables containing the iwconfing output for each wifi device
607 function wifi.getiwconfig()
608 local cnt = luci.util.exec("PATH=/sbin:/usr/sbin iwconfig 2>/dev/null")
611 for i, l in pairs(luci.util.split(luci.util.trim(cnt), "\n\n")) do
612 local k = l:match("^(.-) ")
613 l = l:gsub("^(.-) +", "", 1)
615 local entry, flags = _parse_mixed_record(l)
626 --- Get iwlist scan output from all wireless devices.
627 -- @return Table of tables contaiing all scan results
628 function wifi.iwscan(iface)
629 local siface = iface or ""
630 local cnt = luci.util.exec("iwlist "..siface.." scan 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("^[^\n]+", "", 1)
636 l = luci.util.trim(l)
639 for j, c in pairs(luci.util.split(l, "\n Cell")) do
640 c = c:gsub("^(.-)- ", "", 1)
641 c = luci.util.split(c, "\n", 7)
642 c = table.concat(c, "\n", 1)
643 local entry, flags = _parse_mixed_record(c)
647 table.insert(iws[k], entry)
652 return iface and (iws[iface] or {}) or iws
655 --- Get available channels from given wireless iface.
656 -- @param iface Wireless interface (optional)
657 -- @return Table of available channels
658 function wifi.channels(iface)
659 local cmd = "iwlist " .. ( iface or "" ) .. " freq 2>/dev/null"
662 local fd = io.popen(cmd)
667 if not ln then break end
668 c, f = ln:match("Channel (%d+) : (%d+%.%d+) GHz")
670 cns[tonumber(c)] = tonumber(f)
676 if not ((pairs(cns))(cns)) then
678 2.412, 2.417, 2.422, 2.427, 2.432, 2.437,
679 2.442, 2.447, 2.452, 2.457, 2.462
687 --- LuCI system utilities / init related functions.
689 -- @name luci.sys.init
691 init.dir = "/etc/init.d/"
693 --- Get the names of all installed init scripts
694 -- @return Table containing the names of all inistalled init scripts
695 function init.names()
697 for _, name in ipairs(luci.fs.glob(init.dir.."*")) do
698 names[#names+1] = luci.fs.basename(name)
703 --- Test whether the given init script is enabled
704 -- @param name Name of the init script
705 -- @return Boolean indicating whether init is enabled
706 function init.enabled(name)
707 if luci.fs.access(init.dir..name) then
708 return ( call(init.dir..name.." enabled") == 0 )
713 --- Get the index of he given init script
714 -- @param name Name of the init script
715 -- @return Numeric index value
716 function init.index(name)
717 if luci.fs.access(init.dir..name) then
718 return call("source "..init.dir..name.."; exit $START")
722 --- Enable the given init script
723 -- @param name Name of the init script
724 -- @return Boolean indicating success
725 function init.enable(name)
726 if luci.fs.access(init.dir..name) then
727 return ( call(init.dir..name.." enable") == 1 )
731 --- Disable the given init script
732 -- @param name Name of the init script
733 -- @return Boolean indicating success
734 function init.disable(name)
735 if luci.fs.access(init.dir..name) then
736 return ( call(init.dir..name.." disable") == 0 )
741 -- Internal functions
743 function _parse_delimited_table(iter, delimiter)
744 delimiter = delimiter or "%s+"
747 local trim = luci.util.trim
748 local split = luci.util.split
750 local keys = split(trim(iter()), delimiter, nil, true)
751 for i, j in pairs(keys) do
752 keys[i] = trim(keys[i])
759 for i, j in pairs(split(line, delimiter, nil, true)) do
765 table.insert(data, row)
771 function _parse_mixed_record(cnt, delimiter)
772 delimiter = delimiter or " "
776 for i, l in pairs(luci.util.split(luci.util.trim(cnt), "\n")) do
777 for j, f in pairs(luci.util.split(luci.util.trim(l), delimiter, nil, true)) do
778 local k, x, v = f:match('([^%s][^:=]*) *([:=]*) *"*([^\n"]*)"*')
782 table.insert(flags, k)