* luci/themes: fix log pages
[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, pcall = tonumber, ipairs, pairs, pcall
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 posix.uname("%n")
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.popen 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 cpuinfo = luci.fs.readfile("/proc/cpuinfo")
179 local meminfo = luci.fs.readfile("/proc/meminfo")
180
181 local system = cpuinfo:match("system typ.-:%s*([^\n]+)")
182 local model = ""
183 local memtotal = tonumber(meminfo:match("MemTotal:%s*(%d+)"))
184 local memcached = tonumber(meminfo:match("\nCached:%s*(%d+)"))
185 local memfree = tonumber(meminfo:match("MemFree:%s*(%d+)"))
186 local membuffers = tonumber(meminfo:match("Buffers:%s*(%d+)"))
187
188 if not system then
189 system = posix.uname("%m")
190 model = cpuinfo:match("model name.-:%s*([^\n]+)")
191 if not model then
192 model = cpuinfo:match("Processor.-:%s*([^\n]+)")
193 end
194 else
195 model = cpuinfo:match("cpu model.-:%s*([^\n]+)")
196 end
197
198 return system, model, memtotal, memcached, membuffers, memfree
199 end
200
201 --- Retrieves the output of the "logread" command.
202 -- @return String containing the current log buffer
203 function syslog()
204 return luci.util.exec("logread")
205 end
206
207 --- Retrieves the output of the "dmesg" command.
208 -- @return String containing the current log buffer
209 function dmesg()
210 return luci.util.exec("dmesg")
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 --- Returns conntrack information
266 -- @return Table with the currently tracked IP connections
267 function net.conntrack()
268 local connt = {}
269 if luci.fs.access("/proc/net/nf_conntrack") then
270 for line in io.lines("/proc/net/nf_conntrack") do
271 local entry, flags = _parse_mixed_record(line, " +")
272 entry.layer3 = flags[1]
273 entry.layer4 = flags[2]
274 for i=1, #entry do
275 entry[i] = nil
276 end
277
278 connt[#connt+1] = entry
279 end
280 elseif luci.fs.access("/proc/net/ip_conntrack") then
281 for line in io.lines("/proc/net/ip_conntrack") do
282 local entry, flags = _parse_mixed_record(line, " +")
283 entry.layer3 = "ipv4"
284 entry.layer4 = flags[1]
285 for i=1, #entry do
286 entry[i] = nil
287 end
288
289 connt[#connt+1] = entry
290 end
291 else
292 return nil
293 end
294 return connt
295 end
296
297 --- Determine the current default route.
298 -- @return Table with the properties of the current default route.
299 -- The following fields are defined:
300 -- { "Mask", "RefCnt", "Iface", "Flags", "Window", "IRTT",
301 -- "MTU", "Gateway", "Destination", "Metric", "Use" }
302 function net.defaultroute()
303 local routes = net.routes()
304 local route = nil
305
306 for i, r in pairs(luci.sys.net.routes()) do
307 if r.Destination == "00000000" and (not route or route.Metric > r.Metric) then
308 route = r
309 end
310 end
311
312 return route
313 end
314
315 --- Determine the names of available network interfaces.
316 -- @return Table containing all current interface names
317 function net.devices()
318 local devices = {}
319 for line in io.lines("/proc/net/dev") do
320 table.insert(devices, line:match(" *(.-):"))
321 end
322 return devices
323 end
324
325
326 --- Return information about available network interfaces.
327 -- @return Table containing all current interface names and their information
328 function net.deviceinfo()
329 local devices = {}
330 for line in io.lines("/proc/net/dev") do
331 local name, data = line:match("^ *(.-): *(.*)$")
332 if name and data then
333 devices[name] = luci.util.split(data, " +", nil, true)
334 end
335 end
336 return devices
337 end
338
339
340 -- Determine the MAC address belonging to the given IP address.
341 -- @param ip IPv4 address
342 -- @return String containing the MAC address or nil if it cannot be found
343 function net.ip4mac(ip)
344 local mac = nil
345
346 for i, l in ipairs(net.arptable()) do
347 if l["IP address"] == ip then
348 mac = l["HW address"]
349 end
350 end
351
352 return mac
353 end
354
355 --- Returns the current kernel routing table entries.
356 -- @return Table of tables with properties of the corresponding routes.
357 -- The following fields are defined for route entry tables:
358 -- { "Mask", "RefCnt", "Iface", "Flags", "Window", "IRTT",
359 -- "MTU", "Gateway", "Destination", "Metric", "Use" }
360 function net.routes()
361 return _parse_delimited_table(io.lines("/proc/net/route"))
362 end
363
364
365 --- Tests whether the given host responds to ping probes.
366 -- @param host String containing a hostname or IPv4 address
367 -- @return Number containing 0 on success and >= 1 on error
368 function net.pingtest(host)
369 return os.execute("ping -c1 '"..host:gsub("'", '').."' >/dev/null 2>&1")
370 end
371
372
373 --- LuCI system utilities / process related functions.
374 -- @class module
375 -- @name luci.sys.process
376 process = {}
377
378 --- Get the current process id.
379 -- @class function
380 -- @name process.info
381 -- @return Number containing the current pid
382 process.info = posix.getpid
383
384 --- Retrieve information about currently running processes.
385 -- @return Table containing process information
386 function process.list()
387 local data = {}
388 local k
389 local ps = luci.util.execi("top -bn1")
390
391 if not ps then
392 return
393 end
394
395 while true do
396 local line = ps()
397 if not line then
398 return
399 end
400
401 k = luci.util.split(luci.util.trim(line), "%s+", nil, true)
402 if k[1] == "PID" then
403 break
404 end
405 end
406
407 for line in ps do
408 local row = {}
409
410 line = luci.util.trim(line)
411 for i, value in ipairs(luci.util.split(line, "%s+", #k-1, true)) do
412 row[k[i]] = value
413 end
414
415 local pid = tonumber(row[k[1]])
416 if pid then
417 data[pid] = row
418 end
419 end
420
421 return data
422 end
423
424 --- Set the gid of a process identified by given pid.
425 -- @param pid Number containing the process id
426 -- @param gid Number containing the Unix group id
427 -- @return Boolean indicating successful operation
428 -- @return String containing the error message if failed
429 -- @return Number containing the error code if failed
430 function process.setgroup(pid, gid)
431 return posix.setpid("g", pid, gid)
432 end
433
434 --- Set the uid of a process identified by given pid.
435 -- @param pid Number containing the process id
436 -- @param uid Number containing the Unix user id
437 -- @return Boolean indicating successful operation
438 -- @return String containing the error message if failed
439 -- @return Number containing the error code if failed
440 function process.setuser(pid, uid)
441 return posix.setpid("u", pid, uid)
442 end
443
444 --- Send a signal to a process identified by given pid.
445 -- @class function
446 -- @name process.signal
447 -- @param pid Number containing the process id
448 -- @param sig Signal to send (default: 15 [SIGTERM])
449 -- @return Boolean indicating successful operation
450 -- @return Number containing the error code if failed
451 process.signal = posix.kill
452
453
454 --- LuCI system utilities / user related functions.
455 -- @class module
456 -- @name luci.sys.user
457 user = {}
458
459 --- Retrieve user informations for given uid.
460 -- @class function
461 -- @name getuser
462 -- @param uid Number containing the Unix user id
463 -- @return Table containing the following fields:
464 -- { "uid", "gid", "name", "passwd", "dir", "shell", "gecos" }
465 user.getuser = posix.getpasswd
466
467 --- Test whether given string matches the password of a given system user.
468 -- @param username String containing the Unix user name
469 -- @param password String containing the password to compare
470 -- @return Boolean indicating wheather the passwords are equal
471 function user.checkpasswd(username, password)
472 local account = user.getuser(username)
473
474 if account then
475 local pwd = account.passwd
476 local shadowpw
477 if #pwd == 1 then
478 if luci.fs.stat("/etc/shadow") then
479 if not pcall(function()
480 for l in io.lines("/etc/shadow") do
481 shadowpw = l:match("^%s:([^:]+)" % username)
482 if shadowpw then
483 pwd = shadowpw
484 break
485 end
486 end
487 end) then
488 return nil, "Unable to access shadow-file"
489 end
490 end
491
492 if pwd == "!" then
493 return true
494 end
495 end
496
497 if pwd and #pwd > 0 and password and #password > 0 then
498 return (pwd == posix.crypt(password, pwd))
499 end
500 end
501
502 return false
503 end
504
505 --- Change the password of given user.
506 -- @param username String containing the Unix user name
507 -- @param password String containing the password to compare
508 -- @return Number containing 0 on success and >= 1 on error
509 function user.setpasswd(username, password)
510 if password then
511 password = password:gsub("'", "")
512 end
513
514 if username then
515 username = username:gsub("'", "")
516 end
517
518 local cmd = "(echo '"..password.."';sleep 1;echo '"..password.."')|"
519 cmd = cmd .. "passwd '"..username.."' >/dev/null 2>&1"
520 return os.execute(cmd)
521 end
522
523
524 --- LuCI system utilities / wifi related functions.
525 -- @class module
526 -- @name luci.sys.wifi
527 wifi = {}
528
529 --- Get iwconfig output for all wireless devices.
530 -- @return Table of tables containing the iwconfing output for each wifi device
531 function wifi.getiwconfig()
532 local cnt = luci.util.exec("/usr/sbin/iwconfig 2>/dev/null")
533 local iwc = {}
534
535 for i, l in pairs(luci.util.split(luci.util.trim(cnt), "\n\n")) do
536 local k = l:match("^(.-) ")
537 l = l:gsub("^(.-) +", "", 1)
538 if k then
539 local entry, flags = _parse_mixed_record(l)
540 if entry then
541 entry.flags = flags
542 end
543 iwc[k] = entry
544 end
545 end
546
547 return iwc
548 end
549
550 --- Get iwlist scan output from all wireless devices.
551 -- @return Table of tables contaiing all scan results
552 function wifi.iwscan(iface)
553 local siface = iface or ""
554 local cnt = luci.util.exec("iwlist "..siface.." scan 2>/dev/null")
555 local iws = {}
556
557 for i, l in pairs(luci.util.split(luci.util.trim(cnt), "\n\n")) do
558 local k = l:match("^(.-) ")
559 l = l:gsub("^[^\n]+", "", 1)
560 l = luci.util.trim(l)
561 if k then
562 iws[k] = {}
563 for j, c in pairs(luci.util.split(l, "\n Cell")) do
564 c = c:gsub("^(.-)- ", "", 1)
565 c = luci.util.split(c, "\n", 7)
566 c = table.concat(c, "\n", 1)
567 local entry, flags = _parse_mixed_record(c)
568 if entry then
569 entry.flags = flags
570 end
571 table.insert(iws[k], entry)
572 end
573 end
574 end
575
576 return iface and (iws[iface] or {}) or iws
577 end
578
579
580 --- LuCI system utilities / init related functions.
581 -- @class module
582 -- @name luci.sys.init
583 init = {}
584 init.dir = "/etc/init.d/"
585
586 --- Get the names of all installed init scripts
587 -- @return Table containing the names of all inistalled init scripts
588 function init.names()
589 local names = { }
590 for _, name in ipairs(luci.fs.glob(init.dir.."*")) do
591 names[#names+1] = luci.fs.basename(name)
592 end
593 return names
594 end
595
596 --- Test whether the given init script is enabled
597 -- @param name Name of the init script
598 -- @return Boolean indicating whether init is enabled
599 function init.enabled(name)
600 if luci.fs.access(init.dir..name) then
601 return ( call(init.dir..name.." enabled") == 0 )
602 end
603 return false
604 end
605
606 --- Get the index of he given init script
607 -- @param name Name of the init script
608 -- @return Numeric index value
609 function init.index(name)
610 if luci.fs.access(init.dir..name) then
611 return call("source "..init.dir..name.."; exit $START")
612 end
613 end
614
615 --- Enable the given init script
616 -- @param name Name of the init script
617 -- @return Boolean indicating success
618 function init.enable(name)
619 if luci.fs.access(init.dir..name) then
620 return ( call(init.dir..name.." enable") == 1 )
621 end
622 end
623
624 --- Disable the given init script
625 -- @param name Name of the init script
626 -- @return Boolean indicating success
627 function init.disable(name)
628 if luci.fs.access(init.dir..name) then
629 return ( call(init.dir..name.." disable") == 0 )
630 end
631 end
632
633
634 -- Internal functions
635
636 function _parse_delimited_table(iter, delimiter)
637 delimiter = delimiter or "%s+"
638
639 local data = {}
640 local trim = luci.util.trim
641 local split = luci.util.split
642
643 local keys = split(trim(iter()), delimiter, nil, true)
644 for i, j in pairs(keys) do
645 keys[i] = trim(keys[i])
646 end
647
648 for line in iter do
649 local row = {}
650 line = trim(line)
651 if #line > 0 then
652 for i, j in pairs(split(line, delimiter, nil, true)) do
653 if keys[i] then
654 row[keys[i]] = j
655 end
656 end
657 end
658 table.insert(data, row)
659 end
660
661 return data
662 end
663
664 function _parse_mixed_record(cnt, delimiter)
665 delimiter = delimiter or " "
666 local data = {}
667 local flags = {}
668
669 for i, l in pairs(luci.util.split(luci.util.trim(cnt), "\n")) do
670 for j, f in pairs(luci.util.split(luci.util.trim(l), delimiter, nil, true)) do
671 local k, x, v = f:match('([^%s][^:=]+) *([:=]*) *"*([^\n"]*)"*')
672
673 if k then
674 if x == "" then
675 table.insert(flags, k)
676 else
677 data[k] = v
678 end
679 end
680 end
681 end
682
683 return data, flags
684 end