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