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