luci-0.8: merge r3714, r3716, r3750 and r3833-r3834
[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 IPv4 default route. If multiple default routes exist,
298 -- return the one with the lowest metric.
299 -- @return Table with the properties of the current default route.
300 -- The following fields are defined:
301 -- { "dest", "gateway", "metric", "refcount", "usecount", "irtt",
302 -- "flags", "device" }
303 function net.defaultroute()
304 local route = nil
305 for _, r in pairs(net.routes()) do
306 if r.dest:prefix() == 0 and (not route or route.metric > r.metric) then
307 route = r
308 end
309 end
310 return route
311 end
312
313 --- Determine the current IPv6 default route. If multiple default routes exist,
314 -- return the one with the lowest metric.
315 -- @return Table with the properties of the current default route.
316 -- The following fields are defined:
317 -- { "source", "dest", "nexthop", "metric", "refcount", "usecount",
318 -- "flags", "device" }
319 function net.defaultroute6()
320 local route = nil
321 for _, r in pairs(net.routes6()) do
322 if r.dest:prefix() == 0 and (not route or route.metric > r.metric) then
323 route = r
324 end
325 end
326 return route
327 end
328
329 --- Determine the names of available network interfaces.
330 -- @return Table containing all current interface names
331 function net.devices()
332 local devices = {}
333 for line in io.lines("/proc/net/dev") do
334 table.insert(devices, line:match(" *(.-):"))
335 end
336 return devices
337 end
338
339
340 --- Return information about available network interfaces.
341 -- @return Table containing all current interface names and their information
342 function net.deviceinfo()
343 local devices = {}
344 for line in io.lines("/proc/net/dev") do
345 local name, data = line:match("^ *(.-): *(.*)$")
346 if name and data then
347 devices[name] = luci.util.split(data, " +", nil, true)
348 end
349 end
350 return devices
351 end
352
353
354 -- Determine the MAC address belonging to the given IP address.
355 -- @param ip IPv4 address
356 -- @return String containing the MAC address or nil if it cannot be found
357 function net.ip4mac(ip)
358 local mac = nil
359
360 for i, l in ipairs(net.arptable()) do
361 if l["IP address"] == ip then
362 mac = l["HW address"]
363 end
364 end
365
366 return mac
367 end
368
369 --- Returns the current kernel routing table entries.
370 -- @return Table of tables with properties of the corresponding routes.
371 -- The following fields are defined for route entry tables:
372 -- { "dest", "gateway", "metric", "refcount", "usecount", "irtt",
373 -- "flags", "device" }
374 function net.routes()
375 local routes = { }
376
377 for line in io.lines("/proc/net/route") do
378
379 local dev, dst_ip, gateway, flags, refcnt, usecnt, metric,
380 dst_mask, mtu, win, irtt = line:match(
381 "([^%s]+)\t([A-F0-9]+)\t([A-F0-9]+)\t([A-F0-9]+)\t" ..
382 "(%d+)\t(%d+)\t(%d+)\t([A-F0-9]+)\t(%d+)\t(%d+)\t(%d+)"
383 )
384
385 if dev then
386 gateway = luci.ip.Hex( gateway, 32, luci.ip.FAMILY_INET4 )
387 dst_mask = luci.ip.Hex( dst_mask, 32, luci.ip.FAMILY_INET4 )
388 dst_ip = luci.ip.Hex(
389 dst_ip, dst_mask:prefix(dst_mask), luci.ip.FAMILY_INET4
390 )
391
392 routes[#routes+1] = {
393 dest = dst_ip,
394 gateway = gateway,
395 metric = tonumber(metric),
396 refcount = tonumber(refcnt),
397 usecount = tonumber(usecnt),
398 mtu = tonumber(mtu),
399 window = tonumber(window),
400 irtt = tonumber(irtt),
401 flags = tonumber(flags, 16),
402 device = dev
403 }
404 end
405 end
406
407 return routes
408 end
409
410 --- Returns the current ipv6 kernel routing table entries.
411 -- @return Table of tables with properties of the corresponding routes.
412 -- The following fields are defined for route entry tables:
413 -- { "source", "dest", "nexthop", "metric", "refcount", "usecount",
414 -- "flags", "device" }
415 function net.routes6()
416 local routes = { }
417
418 for line in io.lines("/proc/net/ipv6_route") do
419
420 local dst_ip, dst_prefix, src_ip, src_prefix, nexthop,
421 metric, refcnt, usecnt, flags, dev = line:match(
422 "([a-f0-9]+) ([a-f0-9]+) " ..
423 "([a-f0-9]+) ([a-f0-9]+) " ..
424 "([a-f0-9]+) ([a-f0-9]+) " ..
425 "([a-f0-9]+) ([a-f0-9]+) " ..
426 "([a-f0-9]+) +([^%s]+)"
427 )
428
429 src_ip = luci.ip.Hex(
430 src_ip, tonumber(src_prefix, 16), luci.ip.FAMILY_INET6, false
431 )
432
433 dst_ip = luci.ip.Hex(
434 dst_ip, tonumber(dst_prefix, 16), luci.ip.FAMILY_INET6, false
435 )
436
437 nexthop = luci.ip.Hex( nexthop, 128, luci.ip.FAMILY_INET6, false )
438
439 routes[#routes+1] = {
440 source = src_ip,
441 dest = dst_ip,
442 nexthop = nexthop,
443 metric = tonumber(metric, 16),
444 refcount = tonumber(refcnt, 16),
445 usecount = tonumber(usecnt, 16),
446 flags = tonumber(flags, 16),
447 device = dev
448 }
449 end
450
451 return routes
452 end
453
454 --- Tests whether the given host responds to ping probes.
455 -- @param host String containing a hostname or IPv4 address
456 -- @return Number containing 0 on success and >= 1 on error
457 function net.pingtest(host)
458 return os.execute("ping -c1 '"..host:gsub("'", '').."' >/dev/null 2>&1")
459 end
460
461
462 --- LuCI system utilities / process related functions.
463 -- @class module
464 -- @name luci.sys.process
465 process = {}
466
467 --- Get the current process id.
468 -- @class function
469 -- @name process.info
470 -- @return Number containing the current pid
471 process.info = posix.getpid
472
473 --- Retrieve information about currently running processes.
474 -- @return Table containing process information
475 function process.list()
476 local data = {}
477 local k
478 local ps = luci.util.execi("top -bn1")
479
480 if not ps then
481 return
482 end
483
484 while true do
485 local line = ps()
486 if not line then
487 return
488 end
489
490 k = luci.util.split(luci.util.trim(line), "%s+", nil, true)
491 if k[1] == "PID" then
492 break
493 end
494 end
495
496 for line in ps do
497 local row = {}
498
499 line = luci.util.trim(line)
500 for i, value in ipairs(luci.util.split(line, "%s+", #k-1, true)) do
501 row[k[i]] = value
502 end
503
504 local pid = tonumber(row[k[1]])
505 if pid then
506 data[pid] = row
507 end
508 end
509
510 return data
511 end
512
513 --- Set the gid of a process identified by given pid.
514 -- @param pid Number containing the process id
515 -- @param gid Number containing the Unix group id
516 -- @return Boolean indicating successful operation
517 -- @return String containing the error message if failed
518 -- @return Number containing the error code if failed
519 function process.setgroup(pid, gid)
520 return posix.setpid("g", pid, gid)
521 end
522
523 --- Set the uid of a process identified by given pid.
524 -- @param pid Number containing the process id
525 -- @param uid Number containing the Unix user id
526 -- @return Boolean indicating successful operation
527 -- @return String containing the error message if failed
528 -- @return Number containing the error code if failed
529 function process.setuser(pid, uid)
530 return posix.setpid("u", pid, uid)
531 end
532
533 --- Send a signal to a process identified by given pid.
534 -- @class function
535 -- @name process.signal
536 -- @param pid Number containing the process id
537 -- @param sig Signal to send (default: 15 [SIGTERM])
538 -- @return Boolean indicating successful operation
539 -- @return Number containing the error code if failed
540 process.signal = posix.kill
541
542
543 --- LuCI system utilities / user related functions.
544 -- @class module
545 -- @name luci.sys.user
546 user = {}
547
548 --- Retrieve user informations for given uid.
549 -- @class function
550 -- @name getuser
551 -- @param uid Number containing the Unix user id
552 -- @return Table containing the following fields:
553 -- { "uid", "gid", "name", "passwd", "dir", "shell", "gecos" }
554 user.getuser = posix.getpasswd
555
556 --- Test whether given string matches the password of a given system user.
557 -- @param username String containing the Unix user name
558 -- @param password String containing the password to compare
559 -- @return Boolean indicating wheather the passwords are equal
560 function user.checkpasswd(username, password)
561 local account = user.getuser(username)
562
563 if account then
564 local pwd = account.passwd
565 local shadowpw
566 if #pwd == 1 then
567 if luci.fs.stat("/etc/shadow") then
568 if not pcall(function()
569 for l in io.lines("/etc/shadow") do
570 shadowpw = l:match("^%s:([^:]+)" % username)
571 if shadowpw then
572 pwd = shadowpw
573 break
574 end
575 end
576 end) then
577 return nil, "Unable to access shadow-file"
578 end
579 end
580
581 if pwd == "!" then
582 return true
583 end
584 end
585
586 if pwd and #pwd > 0 and password and #password > 0 then
587 return (pwd == posix.crypt(password, pwd))
588 end
589 end
590
591 return false
592 end
593
594 --- Change the password of given user.
595 -- @param username String containing the Unix user name
596 -- @param password String containing the password to compare
597 -- @return Number containing 0 on success and >= 1 on error
598 function user.setpasswd(username, password)
599 if password then
600 password = password:gsub("'", "")
601 end
602
603 if username then
604 username = username:gsub("'", "")
605 end
606
607 local cmd = "(echo '"..password.."';sleep 1;echo '"..password.."')|"
608 cmd = cmd .. "passwd '"..username.."' >/dev/null 2>&1"
609 return os.execute(cmd)
610 end
611
612
613 --- LuCI system utilities / wifi related functions.
614 -- @class module
615 -- @name luci.sys.wifi
616 wifi = {}
617
618 --- Get iwconfig output for all wireless devices.
619 -- @return Table of tables containing the iwconfing output for each wifi device
620 function wifi.getiwconfig()
621 local cnt = luci.util.exec("/usr/sbin/iwconfig 2>/dev/null")
622 local iwc = {}
623
624 for i, l in pairs(luci.util.split(luci.util.trim(cnt), "\n\n")) do
625 local k = l:match("^(.-) ")
626 l = l:gsub("^(.-) +", "", 1)
627 if k then
628 local entry, flags = _parse_mixed_record(l)
629 if entry then
630 entry.flags = flags
631 end
632 iwc[k] = entry
633 end
634 end
635
636 return iwc
637 end
638
639 --- Get iwlist scan output from all wireless devices.
640 -- @return Table of tables contaiing all scan results
641 function wifi.iwscan(iface)
642 local siface = iface or ""
643 local cnt = luci.util.exec("iwlist "..siface.." scan 2>/dev/null")
644 local iws = {}
645
646 for i, l in pairs(luci.util.split(luci.util.trim(cnt), "\n\n")) do
647 local k = l:match("^(.-) ")
648 l = l:gsub("^[^\n]+", "", 1)
649 l = luci.util.trim(l)
650 if k then
651 iws[k] = {}
652 for j, c in pairs(luci.util.split(l, "\n Cell")) do
653 c = c:gsub("^(.-)- ", "", 1)
654 c = luci.util.split(c, "\n", 7)
655 c = table.concat(c, "\n", 1)
656 local entry, flags = _parse_mixed_record(c)
657 if entry then
658 entry.flags = flags
659 end
660 table.insert(iws[k], entry)
661 end
662 end
663 end
664
665 return iface and (iws[iface] or {}) or iws
666 end
667
668
669 --- LuCI system utilities / init related functions.
670 -- @class module
671 -- @name luci.sys.init
672 init = {}
673 init.dir = "/etc/init.d/"
674
675 --- Get the names of all installed init scripts
676 -- @return Table containing the names of all inistalled init scripts
677 function init.names()
678 local names = { }
679 for _, name in ipairs(luci.fs.glob(init.dir.."*")) do
680 names[#names+1] = luci.fs.basename(name)
681 end
682 return names
683 end
684
685 --- Test whether the given init script is enabled
686 -- @param name Name of the init script
687 -- @return Boolean indicating whether init is enabled
688 function init.enabled(name)
689 if luci.fs.access(init.dir..name) then
690 return ( call(init.dir..name.." enabled") == 0 )
691 end
692 return false
693 end
694
695 --- Get the index of he given init script
696 -- @param name Name of the init script
697 -- @return Numeric index value
698 function init.index(name)
699 if luci.fs.access(init.dir..name) then
700 return call("source "..init.dir..name.."; exit $START")
701 end
702 end
703
704 --- Enable the given init script
705 -- @param name Name of the init script
706 -- @return Boolean indicating success
707 function init.enable(name)
708 if luci.fs.access(init.dir..name) then
709 return ( call(init.dir..name.." enable") == 1 )
710 end
711 end
712
713 --- Disable the given init script
714 -- @param name Name of the init script
715 -- @return Boolean indicating success
716 function init.disable(name)
717 if luci.fs.access(init.dir..name) then
718 return ( call(init.dir..name.." disable") == 0 )
719 end
720 end
721
722
723 -- Internal functions
724
725 function _parse_delimited_table(iter, delimiter)
726 delimiter = delimiter or "%s+"
727
728 local data = {}
729 local trim = luci.util.trim
730 local split = luci.util.split
731
732 local keys = split(trim(iter()), delimiter, nil, true)
733 for i, j in pairs(keys) do
734 keys[i] = trim(keys[i])
735 end
736
737 for line in iter do
738 local row = {}
739 line = trim(line)
740 if #line > 0 then
741 for i, j in pairs(split(line, delimiter, nil, true)) do
742 if keys[i] then
743 row[keys[i]] = j
744 end
745 end
746 end
747 table.insert(data, row)
748 end
749
750 return data
751 end
752
753 function _parse_mixed_record(cnt, delimiter)
754 delimiter = delimiter or " "
755 local data = {}
756 local flags = {}
757
758 for i, l in pairs(luci.util.split(luci.util.trim(cnt), "\n")) do
759 for j, f in pairs(luci.util.split(luci.util.trim(l), delimiter, nil, true)) do
760 local k, x, v = f:match('([^%s][^:=]+) *([:=]*) *"*([^\n"]*)"*')
761
762 if k then
763 if x == "" then
764 table.insert(flags, k)
765 else
766 data[k] = v
767 end
768 end
769 end
770 end
771
772 return data, flags
773 end