557a7ce54c7347d8d2f4e4ee8312d838122ad75f
[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 -- @param group Group ID or name of a system user group
211 -- @return      Table with information about the requested group
212 group.getgroup = posix.getgroup
213
214
215 --- LuCI system utilities / network related functions.
216 -- @class       module
217 -- @name        luci.sys.net
218 net = {}
219
220 --- Returns the current arp-table entries as two-dimensional table.
221 -- @return      Table of table containing the current arp entries.
222 --                      The following fields are defined for arp entry objects:
223 --                      { "IP address", "HW address", "HW type", "Flags", "Mask", "Device" }
224 function net.arptable()
225         return _parse_delimited_table(io.lines("/proc/net/arp"), "%s%s+")
226 end
227
228 --- Determine the current default route.
229 -- @return      Table with the properties of the current default route.
230 --                      The following fields are defined:
231 --                      { "Mask", "RefCnt", "Iface", "Flags", "Window", "IRTT",
232 --                        "MTU", "Gateway", "Destination", "Metric", "Use" }
233 function net.defaultroute()
234         local routes = net.routes()
235         local route = nil
236
237         for i, r in pairs(luci.sys.net.routes()) do
238                 if r.Destination == "00000000" and (not route or route.Metric > r.Metric) then
239                         route = r
240                 end
241         end
242
243         return route
244 end
245
246 --- Determine the names of available network interfaces.
247 -- @return      Table containing all current interface names
248 function net.devices()
249         local devices = {}
250         for line in io.lines("/proc/net/dev") do
251                 table.insert(devices, line:match(" *(.-):"))
252         end
253         return devices
254 end
255
256
257 --- Return information about available network interfaces.
258 -- @return      Table containing all current interface names and their information
259 function net.deviceinfo()
260         local devices = {}
261         for line in io.lines("/proc/net/dev") do
262                 local name, data = line:match("^ *(.-): *(.*)$")
263                 if name and data then
264                         devices[name] = luci.util.split(data, " +", nil, true)
265                 end
266         end
267         return devices
268 end
269
270
271 -- Determine the MAC address belonging to the given IP address.
272 -- @param ip    IPv4 address
273 -- @return              String containing the MAC address or nil if it cannot be found
274 function net.ip4mac(ip)
275         local mac = nil
276
277         for i, l in ipairs(net.arptable()) do
278                 if l["IP address"] == ip then
279                         mac = l["HW address"]
280                 end
281         end
282
283         return mac
284 end
285
286 --- Returns the current kernel routing table entries.
287 -- @return      Table of tables with properties of the corresponding routes.
288 --                      The following fields are defined for route entry tables:
289 --                      { "Mask", "RefCnt", "Iface", "Flags", "Window", "IRTT",
290 --                        "MTU", "Gateway", "Destination", "Metric", "Use" }
291 function net.routes()
292         return _parse_delimited_table(io.lines("/proc/net/route"))
293 end
294
295
296 --- Tests whether the given host responds to ping probes.
297 -- @param host  String containing a hostname or IPv4 address
298 -- @return              Number containing 0 on success and >= 1 on error
299 function net.pingtest(host)
300         return os.execute("ping -c1 '"..host:gsub("'", '').."' >/dev/null 2>&1")
301 end
302
303
304 --- LuCI system utilities / process related functions.
305 -- @class       module
306 -- @name        luci.sys.process
307 process = {}
308
309 --- Get the current process id.
310 -- @return      Number containing the current pid
311 process.info = posix.getpid
312
313 --- Retrieve information about currently running processes.
314 -- @return      Table containing process information
315 function process.list()
316         local data = {}
317         local k
318         local ps = luci.util.execi("top -bn1")
319         
320         if not ps then
321                 return
322         end
323         
324         while true do
325                 local line = ps()
326                 if not line then
327                         return
328                 end
329                 
330                 k = luci.util.split(luci.util.trim(line), "%s+", nil, true)
331                 if k[1] == "PID" then
332                         break
333                 end
334         end
335         
336         for line in ps do
337                 local row = {}
338                 
339                 line = luci.util.trim(line)
340                 for i, value in ipairs(luci.util.split(line, "%s+", #k-1, true)) do
341                         row[k[i]] = value
342                 end
343                 
344                 local pid = tonumber(row[k[1]])
345                 if pid then
346                         data[pid] = row
347                 end
348         end
349         
350         return data
351 end
352
353 --- Set the gid of a process identified by given pid.
354 -- @param pid   Number containing the process id
355 -- @param gid   Number containing the Unix group id
356 -- @return              Boolean indicating successful operation
357 -- @return              String containing the error message if failed
358 -- @return              Number containing the error code if failed
359 function process.setgroup(pid, gid)
360         return posix.setpid("g", pid, gid)
361 end
362
363 --- Set the uid of a process identified by given pid.
364 -- @param pid   Number containing the process id
365 -- @param uid   Number containing the Unix user id
366 -- @return              Boolean indicating successful operation
367 -- @return              String containing the error message if failed
368 -- @return              Number containing the error code if failed
369 function process.setuser(pid, uid)
370         return posix.setpid("u", pid, uid)
371 end
372
373 --- Send a signal to a process identified by given pid.
374 -- @param pid   Number containing the process id
375 -- @param sig   Signal to send (default: 15 [SIGTERM])
376 -- @return              Boolean indicating successful operation
377 -- @return              Number containing the error code if failed
378 process.signal = posix.kill
379
380
381 --- LuCI system utilities / user related functions.
382 -- @class       module
383 -- @name        luci.sys.user
384 user = {}
385
386 --- Retrieve user informations for given uid.
387 -- @class               function
388 -- @name                getuser
389 -- @param uid   Number containing the Unix user id
390 -- @return              Table containing the following fields:
391 --                              { "uid", "gid", "name", "passwd", "dir", "shell", "gecos" }
392 user.getuser = posix.getpasswd
393
394 --- Test whether given string matches the password of a given system user.
395 -- @param username      String containing the Unix user name
396 -- @param password      String containing the password to compare
397 -- @return                      Boolean indicating wheather the passwords are equal
398 function user.checkpasswd(username, password)
399         local account = user.getuser(username)
400
401         if account then
402                 if account.passwd == "!" then
403                         return true
404                 else
405                         return (account.passwd == posix.crypt(password, account.passwd))
406                 end
407         end
408 end
409
410 --- Change the password of given user.
411 -- @param username      String containing the Unix user name
412 -- @param password      String containing the password to compare
413 -- @return                      Number containing 0 on success and >= 1 on error
414 function user.setpasswd(username, password)
415         if password then
416                 password = password:gsub("'", "")
417         end
418
419         if username then
420                 username = username:gsub("'", "")
421         end
422
423         local cmd = "(echo '"..password.."';sleep 1;echo '"..password.."')|"
424         cmd = cmd .. "passwd '"..username.."' >/dev/null 2>&1"
425         return os.execute(cmd)
426 end
427
428
429 --- LuCI system utilities / wifi related functions.
430 -- @class       module
431 -- @name        luci.sys.wifi
432 wifi = {}
433
434 --- Get iwconfig output for all wireless devices.
435 -- @return      Table of tables containing the iwconfing output for each wifi device
436 function wifi.getiwconfig()
437         local cnt = luci.util.exec("/usr/sbin/iwconfig 2>/dev/null")
438         local iwc = {}
439
440         for i, l in pairs(luci.util.split(luci.util.trim(cnt), "\n\n")) do
441                 local k = l:match("^(.-) ")
442                 l = l:gsub("^(.-) +", "", 1)
443                 if k then
444                         iwc[k] = _parse_mixed_record(l)
445                 end
446         end
447
448         return iwc
449 end
450
451 --- Get iwlist scan output from all wireless devices.
452 -- @return      Table of tables contaiing all scan results
453 function wifi.iwscan(iface)
454         local siface = iface or ""
455         local cnt = luci.util.exec("iwlist "..siface.." scan 2>/dev/null")
456         local iws = {}
457
458         for i, l in pairs(luci.util.split(luci.util.trim(cnt), "\n\n")) do
459                 local k = l:match("^(.-) ")
460                 l = l:gsub("^[^\n]+", "", 1)
461                 l = luci.util.trim(l)
462                 if k then
463                         iws[k] = {}
464                         for j, c in pairs(luci.util.split(l, "\n          Cell")) do
465                                 c = c:gsub("^(.-)- ", "", 1)
466                                 c = luci.util.split(c, "\n", 7)
467                                 c = table.concat(c, "\n", 1)
468                                 table.insert(iws[k], _parse_mixed_record(c))
469                         end
470                 end
471         end
472
473         return iface and (iws[iface] or {}) or iws
474 end
475
476
477 -- Internal functions
478
479 function _parse_delimited_table(iter, delimiter)
480         delimiter = delimiter or "%s+"
481
482         local data  = {}
483         local trim  = luci.util.trim
484         local split = luci.util.split
485
486         local keys = split(trim(iter()), delimiter, nil, true)
487         for i, j in pairs(keys) do
488                 keys[i] = trim(keys[i])
489         end
490
491         for line in iter do
492                 local row = {}
493                 line = trim(line)
494                 if #line > 0 then
495                         for i, j in pairs(split(line, delimiter, nil, true)) do
496                                 if keys[i] then
497                                         row[keys[i]] = j
498                                 end
499                         end
500                 end
501                 table.insert(data, row)
502         end
503
504         return data
505 end
506
507 function _parse_mixed_record(cnt)
508         local data = {}
509
510         for i, l in pairs(luci.util.split(luci.util.trim(cnt), "\n")) do
511         for j, f in pairs(luci.util.split(luci.util.trim(l), "  ")) do
512                 local k, x, v = f:match('([^%s][^:=]+) *([:=]*) *"*([^\n"]*)"*')
513
514             if k then
515                                 if x == "" then
516                                         table.insert(data, k)
517                                 else
518                         data[k] = v
519                                 end
520             end
521         end
522         end
523
524     return data
525 end