X-Git-Url: https://git.archive.openwrt.org/?p=project%2Fluci.git;a=blobdiff_plain;f=libs%2Fsys%2Fluasrc%2Fsys.lua;h=3412b32b703983397bd59f91df210521ae16af94;hp=970dcffaaa5b27ed17fe722c75da2a6e5f380126;hb=98996e380a7301ec5127de83e047d0bf8cf3df43;hpb=510fde01188d1a9a903c2d56b83f498a1e6666b8 diff --git a/libs/sys/luasrc/sys.lua b/libs/sys/luasrc/sys.lua index 970dcffaa..3412b32b7 100644 --- a/libs/sys/luasrc/sys.lua +++ b/libs/sys/luasrc/sys.lua @@ -24,14 +24,42 @@ limitations under the License. ]]-- + +local io = require "io" +local os = require "os" +local posix = require "posix" +local table = require "table" + +local luci = {} +luci.util = require "luci.util" +luci.fs = require "luci.fs" +luci.ip = require "luci.ip" + +local tonumber, ipairs, pairs, pcall, type = + tonumber, ipairs, pairs, pcall, type + + --- LuCI Linux and POSIX system utilities. -module("luci.sys", package.seeall) -require("posix") -require("luci.util") -require("luci.fs") -require("luci.ip") +module "luci.sys" + +--- Execute a given shell command and return the error code +-- @class function +-- @name call +-- @param ... Command to call +-- @return Error code of the command +function call(...) + return os.execute(...) / 256 +end + +--- Execute a given shell command and capture its standard output +-- @class function +-- @name exec +-- @param command Command to call +-- @return String containg the return the output of the command +exec = luci.util.exec --- Invoke the luci-flash executable to write an image to the flash memory. +-- @param image Local path or URL to image file -- @param kpattern Pattern of files to keep over flash process -- @return Return value of os.execute() function flash(image, kpattern) @@ -50,27 +78,44 @@ function mounts() local data = {} local k = {"fs", "blocks", "used", "available", "percent", "mountpoint"} local ps = luci.util.execi("df") - + if not ps then return else ps() end - + for line in ps do local row = {} - + local j = 1 for value in line:gmatch("[^%s]+") do row[k[j]] = value j = j + 1 end - + if row[k[1]] then + + -- this is a rather ugly workaround to cope with wrapped lines in + -- the df output: + -- + -- /dev/scsi/host0/bus0/target0/lun0/part3 + -- 114382024 93566472 15005244 86% /mnt/usb + -- + + if not row[k[2]] then + j = 2 + line = ps() + for value in line:gmatch("[^%s]+") do + row[k[j]] = value + j = j + 1 + end + end + table.insert(data, row) end end - + return data end @@ -85,19 +130,31 @@ end -- @return Table containing all variables if no variable name is given getenv = posix.getenv ---- Determine the current hostname. +--- Get or set the current hostname. +-- @param String containing a new hostname to set (optional) -- @return String containing the system hostname -function hostname() - return io.lines("/proc/sys/kernel/hostname")() +function hostname(newname) + if type(newname) == "string" and #newname > 0 then + luci.fs.writefile( "/proc/sys/kernel/hostname", newname .. "\n" ) + return newname + else + return posix.uname("%n") + end end --- Returns the contents of a documented referred by an URL. -- @param url The URL to retrieve -- @param stream Return a stream instead of a buffer +-- @param target Directly write to target file name -- @return String containing the contents of given the URL -function httpget(url, stream) - local source = stream and io.open or luci.util.exec - return source("wget -qO- '"..url:gsub("'", "").."'") +function httpget(url, stream, target) + if not target then + local source = stream and io.popen or luci.util.exec + return source("wget -qO- '"..url:gsub("'", "").."'") + else + return os.execute("wget -qO '%s' '%s'" % + {target:gsub("'", ""), url:gsub("'", "")}) + end end --- Returns the system load average values. @@ -125,27 +182,24 @@ end -- @return String containing the memory used for buffering in kB -- @return String containing the free memory amount in kB function sysinfo() - local c1 = "cat /proc/cpuinfo|grep system\\ typ|cut -d: -f2 2>/dev/null" - local c2 = "uname -m 2>/dev/null" - local c3 = "cat /proc/cpuinfo|grep model\\ name|cut -d: -f2 2>/dev/null" - local c4 = "cat /proc/cpuinfo|grep cpu\\ model|cut -d: -f2 2>/dev/null" - local c5 = "cat /proc/meminfo|grep MemTotal|awk {' print $2 '} 2>/dev/null" - local c6 = "cat /proc/meminfo|grep ^Cached|awk {' print $2 '} 2>/dev/null" - local c7 = "cat /proc/meminfo|grep MemFree|awk {' print $2 '} 2>/dev/null" - local c8 = "cat /proc/meminfo|grep Buffers|awk {' print $2 '} 2>/dev/null" - - local system = luci.util.trim(luci.util.exec(c1)) + local cpuinfo = luci.fs.readfile("/proc/cpuinfo") + local meminfo = luci.fs.readfile("/proc/meminfo") + + local system = cpuinfo:match("system typ.-:%s*([^\n]+)") local model = "" - local memtotal = tonumber(luci.util.trim(luci.util.exec(c5))) - local memcached = tonumber(luci.util.trim(luci.util.exec(c6))) - local memfree = tonumber(luci.util.trim(luci.util.exec(c7))) - local membuffers = tonumber(luci.util.trim(luci.util.exec(c8))) - - if system == "" then - system = luci.util.trim(luci.util.exec(c2)) - model = luci.util.trim(luci.util.exec(c3)) + local memtotal = tonumber(meminfo:match("MemTotal:%s*(%d+)")) + local memcached = tonumber(meminfo:match("\nCached:%s*(%d+)")) + local memfree = tonumber(meminfo:match("MemFree:%s*(%d+)")) + local membuffers = tonumber(meminfo:match("Buffers:%s*(%d+)")) + + if not system then + system = posix.uname("%m") + model = cpuinfo:match("model name.-:%s*([^\n]+)") + if not model then + model = cpuinfo:match("Processor.-:%s*([^\n]+)") + end else - model = luci.util.trim(luci.util.exec(c4)) + model = cpuinfo:match("cpu model.-:%s*([^\n]+)") end return system, model, memtotal, memcached, membuffers, memfree @@ -157,6 +211,12 @@ function syslog() return luci.util.exec("logread") end +--- Retrieves the output of the "dmesg" command. +-- @return String containing the current log buffer +function dmesg() + return luci.util.exec("dmesg") +end + --- Generates a random id with specified length. -- @param bytes Number of bytes for the unique id -- @return String containing hex encoded id @@ -189,6 +249,8 @@ end group = {} --- Returns information about a POSIX user group. +-- @class function +-- @name getgroup -- @param group Group ID or name of a system user group -- @return Table with information about the requested group group.getgroup = posix.getgroup @@ -207,21 +269,74 @@ function net.arptable() return _parse_delimited_table(io.lines("/proc/net/arp"), "%s%s+") end ---- Determine the current default route. +--- Returns conntrack information +-- @return Table with the currently tracked IP connections +function net.conntrack() + local connt = {} + if luci.fs.access("/proc/net/nf_conntrack", "r") then + for line in io.lines("/proc/net/nf_conntrack") do + line = line:match "^(.-( [^ =]+=).-)%2" + local entry, flags = _parse_mixed_record(line, " +") + entry.layer3 = flags[1] + entry.layer4 = flags[3] + for i=1, #entry do + entry[i] = nil + end + + connt[#connt+1] = entry + end + elseif luci.fs.access("/proc/net/ip_conntrack", "r") then + for line in io.lines("/proc/net/ip_conntrack") do + line = line:match "^(.-( [^ =]+=).-)%2" + local entry, flags = _parse_mixed_record(line, " +") + entry.layer3 = "ipv4" + entry.layer4 = flags[1] + for i=1, #entry do + entry[i] = nil + end + + connt[#connt+1] = entry + end + else + return nil + end + return connt +end + +--- Determine the current IPv4 default route. If multiple default routes exist, +-- return the one with the lowest metric. -- @return Table with the properties of the current default route. -- The following fields are defined: --- { "Mask", "RefCnt", "Iface", "Flags", "Window", "IRTT", --- "MTU", "Gateway", "Destination", "Metric", "Use" } +-- { "dest", "gateway", "metric", "refcount", "usecount", "irtt", +-- "flags", "device" } function net.defaultroute() - local routes = net.routes() local route = nil - - for i, r in pairs(luci.sys.net.routes()) do - if r.Destination == "00000000" and (not route or route.Metric > r.Metric) then + for _, r in pairs(net.routes()) do + if r.dest:prefix() == 0 and (not route or route.metric > r.metric) then route = r end end + return route +end +--- Determine the current IPv6 default route. If multiple default routes exist, +-- return the one with the lowest metric. +-- @return Table with the properties of the current default route. +-- The following fields are defined: +-- { "source", "dest", "nexthop", "metric", "refcount", "usecount", +-- "flags", "device" } +function net.defaultroute6() + local route = nil + local routes6 = net.routes6() + if routes6 then + for _, r in pairs(routes6) do + if r.dest:prefix() == 0 and + (not route or route.metric > r.metric) + then + route = r + end + end + end return route end @@ -268,12 +383,89 @@ end --- Returns the current kernel routing table entries. -- @return Table of tables with properties of the corresponding routes. -- The following fields are defined for route entry tables: --- { "Mask", "RefCnt", "Iface", "Flags", "Window", "IRTT", --- "MTU", "Gateway", "Destination", "Metric", "Use" } +-- { "dest", "gateway", "metric", "refcount", "usecount", "irtt", +-- "flags", "device" } function net.routes() - return _parse_delimited_table(io.lines("/proc/net/route")) + local routes = { } + + for line in io.lines("/proc/net/route") do + + local dev, dst_ip, gateway, flags, refcnt, usecnt, metric, + dst_mask, mtu, win, irtt = line:match( + "([^%s]+)\t([A-F0-9]+)\t([A-F0-9]+)\t([A-F0-9]+)\t" .. + "(%d+)\t(%d+)\t(%d+)\t([A-F0-9]+)\t(%d+)\t(%d+)\t(%d+)" + ) + + if dev then + gateway = luci.ip.Hex( gateway, 32, luci.ip.FAMILY_INET4 ) + dst_mask = luci.ip.Hex( dst_mask, 32, luci.ip.FAMILY_INET4 ) + dst_ip = luci.ip.Hex( + dst_ip, dst_mask:prefix(dst_mask), luci.ip.FAMILY_INET4 + ) + + routes[#routes+1] = { + dest = dst_ip, + gateway = gateway, + metric = tonumber(metric), + refcount = tonumber(refcnt), + usecount = tonumber(usecnt), + mtu = tonumber(mtu), + window = tonumber(window), + irtt = tonumber(irtt), + flags = tonumber(flags, 16), + device = dev + } + end + end + + return routes end +--- Returns the current ipv6 kernel routing table entries. +-- @return Table of tables with properties of the corresponding routes. +-- The following fields are defined for route entry tables: +-- { "source", "dest", "nexthop", "metric", "refcount", "usecount", +-- "flags", "device" } +function net.routes6() + if luci.fs.access("/proc/net/ipv6_route", "r") then + local routes = { } + + for line in io.lines("/proc/net/ipv6_route") do + + local dst_ip, dst_prefix, src_ip, src_prefix, nexthop, + metric, refcnt, usecnt, flags, dev = line:match( + "([a-f0-9]+) ([a-f0-9]+) " .. + "([a-f0-9]+) ([a-f0-9]+) " .. + "([a-f0-9]+) ([a-f0-9]+) " .. + "([a-f0-9]+) ([a-f0-9]+) " .. + "([a-f0-9]+) +([^%s]+)" + ) + + src_ip = luci.ip.Hex( + src_ip, tonumber(src_prefix, 16), luci.ip.FAMILY_INET6, false + ) + + dst_ip = luci.ip.Hex( + dst_ip, tonumber(dst_prefix, 16), luci.ip.FAMILY_INET6, false + ) + + nexthop = luci.ip.Hex( nexthop, 128, luci.ip.FAMILY_INET6, false ) + + routes[#routes+1] = { + source = src_ip, + dest = dst_ip, + nexthop = nexthop, + metric = tonumber(metric, 16), + refcount = tonumber(refcnt, 16), + usecount = tonumber(usecnt, 16), + flags = tonumber(flags, 16), + device = dev + } + end + + return routes + end +end --- Tests whether the given host responds to ping probes. -- @param host String containing a hostname or IPv4 address @@ -289,6 +481,8 @@ end process = {} --- Get the current process id. +-- @class function +-- @name process.info -- @return Number containing the current pid process.info = posix.getpid @@ -298,37 +492,37 @@ function process.list() local data = {} local k local ps = luci.util.execi("top -bn1") - + if not ps then return end - + while true do local line = ps() if not line then return end - + k = luci.util.split(luci.util.trim(line), "%s+", nil, true) if k[1] == "PID" then break end end - + for line in ps do local row = {} - + line = luci.util.trim(line) for i, value in ipairs(luci.util.split(line, "%s+", #k-1, true)) do row[k[i]] = value end - + local pid = tonumber(row[k[1]]) if pid then data[pid] = row end end - + return data end @@ -353,6 +547,8 @@ function process.setuser(pid, uid) end --- Send a signal to a process identified by given pid. +-- @class function +-- @name process.signal -- @param pid Number containing the process id -- @param sig Signal to send (default: 15 [SIGTERM]) -- @return Boolean indicating successful operation @@ -381,12 +577,34 @@ function user.checkpasswd(username, password) local account = user.getuser(username) if account then - if account.passwd == "!" then - return true - else - return (account.passwd == posix.crypt(password, account.passwd)) + local pwd = account.passwd + local shadowpw + if #pwd == 1 then + if luci.fs.stat("/etc/shadow") then + if not pcall(function() + for l in io.lines("/etc/shadow") do + shadowpw = l:match("^%s:([^:]+)" % username) + if shadowpw then + pwd = shadowpw + break + end + end + end) then + return nil, "Unable to access shadow-file" + end + end + + if pwd == "!" then + return true + end + end + + if pwd and #pwd > 0 and password and #password > 0 then + return (pwd == posix.crypt(password, pwd)) end end + + return false end --- Change the password of given user. @@ -416,14 +634,18 @@ wifi = {} --- Get iwconfig output for all wireless devices. -- @return Table of tables containing the iwconfing output for each wifi device function wifi.getiwconfig() - local cnt = luci.util.exec("/usr/sbin/iwconfig 2>/dev/null") + local cnt = luci.util.exec("PATH=/sbin:/usr/sbin iwconfig 2>/dev/null") local iwc = {} for i, l in pairs(luci.util.split(luci.util.trim(cnt), "\n\n")) do local k = l:match("^(.-) ") l = l:gsub("^(.-) +", "", 1) if k then - iwc[k] = _parse_mixed_record(l) + local entry, flags = _parse_mixed_record(l) + if entry then + entry.flags = flags + end + iwc[k] = entry end end @@ -432,8 +654,9 @@ end --- Get iwlist scan output from all wireless devices. -- @return Table of tables contaiing all scan results -function wifi.iwscan() - local cnt = luci.util.exec("iwlist scan 2>/dev/null") +function wifi.iwscan(iface) + local siface = iface or "" + local cnt = luci.util.exec("iwlist "..siface.." scan 2>/dev/null") local iws = {} for i, l in pairs(luci.util.split(luci.util.trim(cnt), "\n\n")) do @@ -446,12 +669,100 @@ function wifi.iwscan() c = c:gsub("^(.-)- ", "", 1) c = luci.util.split(c, "\n", 7) c = table.concat(c, "\n", 1) - table.insert(iws[k], _parse_mixed_record(c)) + local entry, flags = _parse_mixed_record(c) + if entry then + entry.flags = flags + end + table.insert(iws[k], entry) end end end - return iws + return iface and (iws[iface] or {}) or iws +end + +--- Get available channels from given wireless iface. +-- @param iface Wireless interface (optional) +-- @return Table of available channels +function wifi.channels(iface) + local cmd = "iwlist " .. ( iface or "" ) .. " freq 2>/dev/null" + local cns = { } + + local fd = io.popen(cmd) + if fd then + local ln, c, f + repeat + ln = fd:read("*l") or "" + c, f = ln:match("Channel (%d+) : (%d+%.%d+) GHz") + if c and f then + cns[tonumber(c)] = tonumber(f) + end + until not ( #ln > 0 ) + fd:close() + end + + if #cns == 0 then + cms = { + 2.412, 2.417, 2.422, 2.427, 2.432, 2.437, + 2.442, 2.447, 2.452, 2.457, 2.462 + } + end + + return cns +end + + +--- LuCI system utilities / init related functions. +-- @class module +-- @name luci.sys.init +init = {} +init.dir = "/etc/init.d/" + +--- Get the names of all installed init scripts +-- @return Table containing the names of all inistalled init scripts +function init.names() + local names = { } + for _, name in ipairs(luci.fs.glob(init.dir.."*")) do + names[#names+1] = luci.fs.basename(name) + end + return names +end + +--- Test whether the given init script is enabled +-- @param name Name of the init script +-- @return Boolean indicating whether init is enabled +function init.enabled(name) + if luci.fs.access(init.dir..name) then + return ( call(init.dir..name.." enabled") == 0 ) + end + return false +end + +--- Get the index of he given init script +-- @param name Name of the init script +-- @return Numeric index value +function init.index(name) + if luci.fs.access(init.dir..name) then + return call("source "..init.dir..name.."; exit $START") + end +end + +--- Enable the given init script +-- @param name Name of the init script +-- @return Boolean indicating success +function init.enable(name) + if luci.fs.access(init.dir..name) then + return ( call(init.dir..name.." enable") == 1 ) + end +end + +--- Disable the given init script +-- @param name Name of the init script +-- @return Boolean indicating success +function init.disable(name) + if luci.fs.access(init.dir..name) then + return ( call(init.dir..name.." disable") == 0 ) + end end @@ -485,22 +796,24 @@ function _parse_delimited_table(iter, delimiter) return data end -function _parse_mixed_record(cnt) +function _parse_mixed_record(cnt, delimiter) + delimiter = delimiter or " " local data = {} + local flags = {} for i, l in pairs(luci.util.split(luci.util.trim(cnt), "\n")) do - for j, f in pairs(luci.util.split(luci.util.trim(l), " ")) do - local k, x, v = f:match('([^%s][^:=]+) *([:=]*) *"*([^\n"]*)"*') + for j, f in pairs(luci.util.split(luci.util.trim(l), delimiter, nil, true)) do + local k, x, v = f:match('([^%s][^:=]*) *([:=]*) *"*([^\n"]*)"*') - if k then + if k then if x == "" then - table.insert(data, k) + table.insert(flags, k) else - data[k] = v + data[k] = v end - end - end + end + end end - return data + return data, flags end