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