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