Disable buffering of sysupgrade output
[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