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