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