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