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