Avoid some blocking issues
[project/luci.git] / libs / sys / luasrc / sys.lua
1 --[[
2 LuCI - System library
3
4 Description:
5 Utilities for interaction with the Linux system
6
7 FileId:
8 $Id$
9
10 License:
11 Copyright 2008 Steven Barth <steven@midlink.org>
12
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
16
17         http://www.apache.org/licenses/LICENSE-2.0
18
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.
24
25 ]]--
26
27
28 local io    = require "io"
29 local os    = require "os"
30 local posix = require "posix"
31 local table = require "table"
32
33 local luci  = {}
34 luci.util   = require "luci.util"
35 luci.fs     = require "luci.fs"
36 luci.ip     = require "luci.ip"
37
38 local tonumber, ipairs, pairs = tonumber, ipairs, pairs
39
40
41 --- LuCI Linux and POSIX system utilities.
42 module "luci.sys"
43
44 --- Execute a given shell command and return the error code
45 -- @class               function
46 -- @name                call
47 -- @param               ...             Command to call
48 -- @return              Error code of the command
49 function call(...)
50         return os.execute(...) / 256
51 end
52
53 --- Execute a given shell command and capture its standard output
54 -- @class               function
55 -- @name                exec
56 -- @param command       Command to call
57 -- @return                      String containg the return the output of the command
58 exec = luci.util.exec
59
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                      boolean indicating status
64 -- @return                      error message if any
65 function flash(image, kpattern)
66         local cmd = "luci-flash "
67         if kpattern then
68                 cmd = cmd .. "-k '" .. kpattern:gsub("'", "") .. "' "
69         end
70         cmd = cmd .. "'" .. image:gsub("'", "") .. "' 2>/dev/null &"
71
72         local fp = io.popen(cmd)
73         fp:setvbuf("no")
74         
75         local line = fp:read()
76         
77         if line == "Invalid image type" then
78                 fp:close()
79                 return false, line
80         else
81                 fp:close()
82                 return true
83         end
84 end
85
86 --- Retrieve information about currently mounted file systems.
87 -- @return      Table containing mount information
88 function mounts()
89         local data = {}
90         local k = {"fs", "blocks", "used", "available", "percent", "mountpoint"}
91         local ps = luci.util.execi("df")
92         
93         if not ps then
94                 return
95         else
96                 ps()
97         end
98         
99         for line in ps do
100                 local row = {}
101                 
102                 local j = 1
103                 for value in line:gmatch("[^%s]+") do
104                         row[k[j]] = value
105                         j = j + 1
106                 end
107                 
108                 if row[k[1]] then
109                         table.insert(data, row)
110                 end
111         end
112         
113         return data
114 end
115
116 --- Retrieve environment variables. If no variable is given then a table
117 -- containing the whole environment is returned otherwise this function returns
118 -- the corresponding string value for the given name or nil if no such variable
119 -- exists.
120 -- @class               function
121 -- @name                getenv
122 -- @param var   Name of the environment variable to retrieve (optional)
123 -- @return              String containg the value of the specified variable
124 -- @return              Table containing all variables if no variable name is given
125 getenv = posix.getenv
126
127 --- Determine the current hostname.
128 -- @return              String containing the system hostname
129 function hostname()
130         return io.lines("/proc/sys/kernel/hostname")()
131 end
132
133 --- Returns the contents of a documented referred by an URL.
134 -- @param url    The URL to retrieve
135 -- @param stream Return a stream instead of a buffer
136 -- @param target Directly write to target file name
137 -- @return              String containing the contents of given the URL
138 function httpget(url, stream, target)
139         if not target then
140                 local source = stream and io.open or luci.util.exec
141                 return source("wget -qO- '"..url:gsub("'", "").."'")
142         else
143                 return os.execute("wget -qO '%s' '%s'" %
144                         {target:gsub("'", ""), url:gsub("'", "")})
145         end
146 end
147
148 --- Returns the system load average values.
149 -- @return      String containing the average load value 1 minute ago
150 -- @return      String containing the average load value 5 minutes ago
151 -- @return      String containing the average load value 15 minutes ago
152 -- @return      String containing the active and total number of processes
153 -- @return      String containing the last used pid
154 function loadavg()
155         local loadavg = io.lines("/proc/loadavg")()
156         return loadavg:match("^(.-) (.-) (.-) (.-) (.-)$")
157 end
158
159 --- Initiate a system reboot.
160 -- @return      Return value of os.execute()
161 function reboot()
162         return os.execute("reboot >/dev/null 2>&1")
163 end
164
165 --- Returns the system type, cpu name and installed physical memory.
166 -- @return      String containing the system or platform identifier
167 -- @return      String containing hardware model information
168 -- @return      String containing the total memory amount in kB
169 -- @return      String containing the memory used for caching in kB
170 -- @return      String containing the memory used for buffering in kB
171 -- @return      String containing the free memory amount in kB
172 function sysinfo()
173         local c1 = "cat /proc/cpuinfo|grep system\\ typ|cut -d: -f2 2>/dev/null"
174         local c2 = "uname -m 2>/dev/null"
175         local c3 = "cat /proc/cpuinfo|grep model\\ name|cut -d: -f2 2>/dev/null"
176         local c4 = "cat /proc/cpuinfo|grep cpu\\ model|cut -d: -f2 2>/dev/null"
177         local c5 = "cat /proc/meminfo|grep MemTotal|awk {' print $2 '} 2>/dev/null"
178         local c6 = "cat /proc/meminfo|grep ^Cached|awk {' print $2 '} 2>/dev/null"
179         local c7 = "cat /proc/meminfo|grep MemFree|awk {' print $2 '} 2>/dev/null"
180         local c8 = "cat /proc/meminfo|grep Buffers|awk {' print $2 '} 2>/dev/null"
181
182         local system = luci.util.trim(luci.util.exec(c1))
183         local model = ""
184         local memtotal = tonumber(luci.util.trim(luci.util.exec(c5)))
185         local memcached = tonumber(luci.util.trim(luci.util.exec(c6)))
186         local memfree = tonumber(luci.util.trim(luci.util.exec(c7)))
187         local membuffers = tonumber(luci.util.trim(luci.util.exec(c8)))
188
189         if system == "" then
190                 system = luci.util.trim(luci.util.exec(c2))
191                 model = luci.util.trim(luci.util.exec(c3))
192         else
193                 model = luci.util.trim(luci.util.exec(c4))
194         end
195
196         return system, model, memtotal, memcached, membuffers, memfree
197 end
198
199 --- Retrieves the output of the "logread" command.
200 -- @return      String containing the current log buffer
201 function syslog()
202         return luci.util.exec("logread")
203 end
204
205 --- Generates a random id with specified length.
206 -- @param bytes Number of bytes for the unique id
207 -- @return              String containing hex encoded id
208 function uniqueid(bytes)
209         local fp    = io.open("/dev/urandom")
210         local chunk = { fp:read(bytes):byte(1, bytes) }
211         fp:close()
212
213         local hex = ""
214
215         local pattern = "%02X"
216         for i, byte in ipairs(chunk) do
217                 hex = hex .. pattern:format(byte)
218         end
219
220         return hex
221 end
222
223 --- Returns the current system uptime stats.
224 -- @return      String containing total uptime in seconds
225 -- @return      String containing idle time in seconds
226 function uptime()
227         local loadavg = io.lines("/proc/uptime")()
228         return loadavg:match("^(.-) (.-)$")
229 end
230
231 --- LuCI system utilities / POSIX user group related functions.
232 -- @class       module
233 -- @name        luci.sys.group
234 group = {}
235
236 --- Returns information about a POSIX user group.
237 -- @class function
238 -- @name                getgroup
239 -- @param group Group ID or name of a system user group
240 -- @return      Table with information about the requested group
241 group.getgroup = posix.getgroup
242
243
244 --- LuCI system utilities / network related functions.
245 -- @class       module
246 -- @name        luci.sys.net
247 net = {}
248
249 --- Returns the current arp-table entries as two-dimensional table.
250 -- @return      Table of table containing the current arp entries.
251 --                      The following fields are defined for arp entry objects:
252 --                      { "IP address", "HW address", "HW type", "Flags", "Mask", "Device" }
253 function net.arptable()
254         return _parse_delimited_table(io.lines("/proc/net/arp"), "%s%s+")
255 end
256
257 --- Determine the current default route.
258 -- @return      Table with the properties of the current default route.
259 --                      The following fields are defined:
260 --                      { "Mask", "RefCnt", "Iface", "Flags", "Window", "IRTT",
261 --                        "MTU", "Gateway", "Destination", "Metric", "Use" }
262 function net.defaultroute()
263         local routes = net.routes()
264         local route = nil
265
266         for i, r in pairs(luci.sys.net.routes()) do
267                 if r.Destination == "00000000" and (not route or route.Metric > r.Metric) then
268                         route = r
269                 end
270         end
271
272         return route
273 end
274
275 --- Determine the names of available network interfaces.
276 -- @return      Table containing all current interface names
277 function net.devices()
278         local devices = {}
279         for line in io.lines("/proc/net/dev") do
280                 table.insert(devices, line:match(" *(.-):"))
281         end
282         return devices
283 end
284
285
286 --- Return information about available network interfaces.
287 -- @return      Table containing all current interface names and their information
288 function net.deviceinfo()
289         local devices = {}
290         for line in io.lines("/proc/net/dev") do
291                 local name, data = line:match("^ *(.-): *(.*)$")
292                 if name and data then
293                         devices[name] = luci.util.split(data, " +", nil, true)
294                 end
295         end
296         return devices
297 end
298
299
300 -- Determine the MAC address belonging to the given IP address.
301 -- @param ip    IPv4 address
302 -- @return              String containing the MAC address or nil if it cannot be found
303 function net.ip4mac(ip)
304         local mac = nil
305
306         for i, l in ipairs(net.arptable()) do
307                 if l["IP address"] == ip then
308                         mac = l["HW address"]
309                 end
310         end
311
312         return mac
313 end
314
315 --- Returns the current kernel routing table entries.
316 -- @return      Table of tables with properties of the corresponding routes.
317 --                      The following fields are defined for route entry tables:
318 --                      { "Mask", "RefCnt", "Iface", "Flags", "Window", "IRTT",
319 --                        "MTU", "Gateway", "Destination", "Metric", "Use" }
320 function net.routes()
321         return _parse_delimited_table(io.lines("/proc/net/route"))
322 end
323
324
325 --- Tests whether the given host responds to ping probes.
326 -- @param host  String containing a hostname or IPv4 address
327 -- @return              Number containing 0 on success and >= 1 on error
328 function net.pingtest(host)
329         return os.execute("ping -c1 '"..host:gsub("'", '').."' >/dev/null 2>&1")
330 end
331
332
333 --- LuCI system utilities / process related functions.
334 -- @class       module
335 -- @name        luci.sys.process
336 process = {}
337
338 --- Get the current process id.
339 -- @class function
340 -- @name  process.info
341 -- @return      Number containing the current pid
342 process.info = posix.getpid
343
344 --- Retrieve information about currently running processes.
345 -- @return      Table containing process information
346 function process.list()
347         local data = {}
348         local k
349         local ps = luci.util.execi("top -bn1")
350         
351         if not ps then
352                 return
353         end
354         
355         while true do
356                 local line = ps()
357                 if not line then
358                         return
359                 end
360                 
361                 k = luci.util.split(luci.util.trim(line), "%s+", nil, true)
362                 if k[1] == "PID" then
363                         break
364                 end
365         end
366         
367         for line in ps do
368                 local row = {}
369                 
370                 line = luci.util.trim(line)
371                 for i, value in ipairs(luci.util.split(line, "%s+", #k-1, true)) do
372                         row[k[i]] = value
373                 end
374                 
375                 local pid = tonumber(row[k[1]])
376                 if pid then
377                         data[pid] = row
378                 end
379         end
380         
381         return data
382 end
383
384 --- Set the gid of a process identified by given pid.
385 -- @param pid   Number containing the process id
386 -- @param gid   Number containing the Unix group id
387 -- @return              Boolean indicating successful operation
388 -- @return              String containing the error message if failed
389 -- @return              Number containing the error code if failed
390 function process.setgroup(pid, gid)
391         return posix.setpid("g", pid, gid)
392 end
393
394 --- Set the uid of a process identified by given pid.
395 -- @param pid   Number containing the process id
396 -- @param uid   Number containing the Unix user id
397 -- @return              Boolean indicating successful operation
398 -- @return              String containing the error message if failed
399 -- @return              Number containing the error code if failed
400 function process.setuser(pid, uid)
401         return posix.setpid("u", pid, uid)
402 end
403
404 --- Send a signal to a process identified by given pid.
405 -- @class function
406 -- @name  process.signal
407 -- @param pid   Number containing the process id
408 -- @param sig   Signal to send (default: 15 [SIGTERM])
409 -- @return              Boolean indicating successful operation
410 -- @return              Number containing the error code if failed
411 process.signal = posix.kill
412
413
414 --- LuCI system utilities / user related functions.
415 -- @class       module
416 -- @name        luci.sys.user
417 user = {}
418
419 --- Retrieve user informations for given uid.
420 -- @class               function
421 -- @name                getuser
422 -- @param uid   Number containing the Unix user id
423 -- @return              Table containing the following fields:
424 --                              { "uid", "gid", "name", "passwd", "dir", "shell", "gecos" }
425 user.getuser = posix.getpasswd
426
427 --- Test whether given string matches the password of a given system user.
428 -- @param username      String containing the Unix user name
429 -- @param password      String containing the password to compare
430 -- @return                      Boolean indicating wheather the passwords are equal
431 function user.checkpasswd(username, password)
432         local account = user.getuser(username)
433
434         if account then
435                 if account.passwd == "!" then
436                         return true
437                 else
438                         return (account.passwd == posix.crypt(password, account.passwd))
439                 end
440         end
441 end
442
443 --- Change the password of given user.
444 -- @param username      String containing the Unix user name
445 -- @param password      String containing the password to compare
446 -- @return                      Number containing 0 on success and >= 1 on error
447 function user.setpasswd(username, password)
448         if password then
449                 password = password:gsub("'", "")
450         end
451
452         if username then
453                 username = username:gsub("'", "")
454         end
455
456         local cmd = "(echo '"..password.."';sleep 1;echo '"..password.."')|"
457         cmd = cmd .. "passwd '"..username.."' >/dev/null 2>&1"
458         return os.execute(cmd)
459 end
460
461
462 --- LuCI system utilities / wifi related functions.
463 -- @class       module
464 -- @name        luci.sys.wifi
465 wifi = {}
466
467 --- Get iwconfig output for all wireless devices.
468 -- @return      Table of tables containing the iwconfing output for each wifi device
469 function wifi.getiwconfig()
470         local cnt = luci.util.exec("/usr/sbin/iwconfig 2>/dev/null")
471         local iwc = {}
472
473         for i, l in pairs(luci.util.split(luci.util.trim(cnt), "\n\n")) do
474                 local k = l:match("^(.-) ")
475                 l = l:gsub("^(.-) +", "", 1)
476                 if k then
477                         iwc[k] = _parse_mixed_record(l)
478                 end
479         end
480
481         return iwc
482 end
483
484 --- Get iwlist scan output from all wireless devices.
485 -- @return      Table of tables contaiing all scan results
486 function wifi.iwscan(iface)
487         local siface = iface or ""
488         local cnt = luci.util.exec("iwlist "..siface.." scan 2>/dev/null")
489         local iws = {}
490
491         for i, l in pairs(luci.util.split(luci.util.trim(cnt), "\n\n")) do
492                 local k = l:match("^(.-) ")
493                 l = l:gsub("^[^\n]+", "", 1)
494                 l = luci.util.trim(l)
495                 if k then
496                         iws[k] = {}
497                         for j, c in pairs(luci.util.split(l, "\n          Cell")) do
498                                 c = c:gsub("^(.-)- ", "", 1)
499                                 c = luci.util.split(c, "\n", 7)
500                                 c = table.concat(c, "\n", 1)
501                                 table.insert(iws[k], _parse_mixed_record(c))
502                         end
503                 end
504         end
505
506         return iface and (iws[iface] or {}) or iws
507 end
508
509
510 -- Internal functions
511
512 function _parse_delimited_table(iter, delimiter)
513         delimiter = delimiter or "%s+"
514
515         local data  = {}
516         local trim  = luci.util.trim
517         local split = luci.util.split
518
519         local keys = split(trim(iter()), delimiter, nil, true)
520         for i, j in pairs(keys) do
521                 keys[i] = trim(keys[i])
522         end
523
524         for line in iter do
525                 local row = {}
526                 line = trim(line)
527                 if #line > 0 then
528                         for i, j in pairs(split(line, delimiter, nil, true)) do
529                                 if keys[i] then
530                                         row[keys[i]] = j
531                                 end
532                         end
533                 end
534                 table.insert(data, row)
535         end
536
537         return data
538 end
539
540 function _parse_mixed_record(cnt)
541         local data = {}
542
543         for i, l in pairs(luci.util.split(luci.util.trim(cnt), "\n")) do
544         for j, f in pairs(luci.util.split(luci.util.trim(l), "  ")) do
545                 local k, x, v = f:match('([^%s][^:=]+) *([:=]*) *"*([^\n"]*)"*')
546
547             if k then
548                                 if x == "" then
549                                         table.insert(data, k)
550                                 else
551                         data[k] = v
552                                 end
553             end
554         end
555         end
556
557     return data
558 end