luci-0.9: merge r5762
[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 table = require "table"
31 local nixio = require "nixio"
32 local fs = require "nixio.fs"
33
34 local luci = {}
35 luci.util = require "luci.util"
36 luci.ip = require "luci.ip"
37
38 local tonumber, ipairs, pairs, pcall, type, next =
39 tonumber, ipairs, pairs, pcall, type, next
40
41
42 --- LuCI Linux and POSIX system utilities.
43 module "luci.sys"
44
45 --- Execute a given shell command and return the error code
46 -- @class function
47 -- @name call
48 -- @param ... Command to call
49 -- @return Error code of the command
50 function call(...)
51 return os.execute(...) / 256
52 end
53
54 --- Execute a given shell command and capture its standard output
55 -- @class function
56 -- @name exec
57 -- @param command Command to call
58 -- @return String containg the return the output of the command
59 exec = luci.util.exec
60
61 --- Invoke the luci-flash executable to write an image to the flash memory.
62 -- @param image Local path or URL to image file
63 -- @param kpattern Pattern of files to keep over flash process
64 -- @return Return value of os.execute()
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("'", "") .. "' >/dev/null 2>&1"
71
72 return os.execute(cmd)
73 end
74
75 --- Retrieve information about currently mounted file systems.
76 -- @return Table containing mount information
77 function mounts()
78 local data = {}
79 local k = {"fs", "blocks", "used", "available", "percent", "mountpoint"}
80 local ps = luci.util.execi("df")
81
82 if not ps then
83 return
84 else
85 ps()
86 end
87
88 for line in ps do
89 local row = {}
90
91 local j = 1
92 for value in line:gmatch("[^%s]+") do
93 row[k[j]] = value
94 j = j + 1
95 end
96
97 if row[k[1]] then
98
99 -- this is a rather ugly workaround to cope with wrapped lines in
100 -- the df output:
101 --
102 -- /dev/scsi/host0/bus0/target0/lun0/part3
103 -- 114382024 93566472 15005244 86% /mnt/usb
104 --
105
106 if not row[k[2]] then
107 j = 2
108 line = ps()
109 for value in line:gmatch("[^%s]+") do
110 row[k[j]] = value
111 j = j + 1
112 end
113 end
114
115 table.insert(data, row)
116 end
117 end
118
119 return data
120 end
121
122 --- Retrieve environment variables. If no variable is given then a table
123 -- containing the whole environment is returned otherwise this function returns
124 -- the corresponding string value for the given name or nil if no such variable
125 -- exists.
126 -- @class function
127 -- @name getenv
128 -- @param var Name of the environment variable to retrieve (optional)
129 -- @return String containg the value of the specified variable
130 -- @return Table containing all variables if no variable name is given
131 getenv = nixio.getenv
132
133 --- Get or set the current hostname.
134 -- @param String containing a new hostname to set (optional)
135 -- @return String containing the system hostname
136 function hostname(newname)
137 if type(newname) == "string" and #newname > 0 then
138 fs.writefile( "/proc/sys/kernel/hostname", newname )
139 return newname
140 else
141 return nixio.uname().nodename
142 end
143 end
144
145 --- Returns the contents of a documented referred by an URL.
146 -- @param url The URL to retrieve
147 -- @param stream Return a stream instead of a buffer
148 -- @param target Directly write to target file name
149 -- @return String containing the contents of given the URL
150 function httpget(url, stream, target)
151 if not target then
152 local source = stream and io.popen or luci.util.exec
153 return source("wget -qO- '"..url:gsub("'", "").."'")
154 else
155 return os.execute("wget -qO '%s' '%s'" %
156 {target:gsub("'", ""), url:gsub("'", "")})
157 end
158 end
159
160 --- Returns the system load average values.
161 -- @return String containing the average load value 1 minute ago
162 -- @return String containing the average load value 5 minutes ago
163 -- @return String containing the average load value 15 minutes ago
164 function loadavg()
165 local info = nixio.sysinfo()
166 return info.loads[1], info.loads[2], info.loads[3]
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 cpuinfo = fs.readfile("/proc/cpuinfo")
184 local meminfo = fs.readfile("/proc/meminfo")
185
186 local system = cpuinfo:match("system typ.-:%s*([^\n]+)")
187 local model = ""
188 local memtotal = tonumber(meminfo:match("MemTotal:%s*(%d+)"))
189 local memcached = tonumber(meminfo:match("\nCached:%s*(%d+)"))
190 local memfree = tonumber(meminfo:match("MemFree:%s*(%d+)"))
191 local membuffers = tonumber(meminfo:match("Buffers:%s*(%d+)"))
192
193 if not system then
194 system = nixio.uname().machine
195 model = cpuinfo:match("model name.-:%s*([^\n]+)")
196 if not model then
197 model = cpuinfo:match("Processor.-:%s*([^\n]+)")
198 end
199 else
200 model = cpuinfo:match("cpu model.-:%s*([^\n]+)")
201 end
202
203 return system, model, memtotal, memcached, membuffers, memfree
204 end
205
206 --- Retrieves the output of the "logread" command.
207 -- @return String containing the current log buffer
208 function syslog()
209 return luci.util.exec("logread")
210 end
211
212 --- Retrieves the output of the "dmesg" command.
213 -- @return String containing the current log buffer
214 function dmesg()
215 return luci.util.exec("dmesg")
216 end
217
218 --- Generates a random id with specified length.
219 -- @param bytes Number of bytes for the unique id
220 -- @return String containing hex encoded id
221 function uniqueid(bytes)
222 local rand = fs.readfile("/dev/urandom", bytes)
223 return rand and nixio.bin.hexlify(rand)
224 end
225
226 --- Returns the current system uptime stats.
227 -- @return String containing total uptime in seconds
228 function uptime()
229 return nixio.sysinfo().uptime
230 end
231
232
233 --- LuCI system utilities / network related functions.
234 -- @class module
235 -- @name luci.sys.net
236 net = {}
237
238 --- Returns the current arp-table entries as two-dimensional table.
239 -- @return Table of table containing the current arp entries.
240 -- The following fields are defined for arp entry objects:
241 -- { "IP address", "HW address", "HW type", "Flags", "Mask", "Device" }
242 function net.arptable(callback)
243 return _parse_delimited_table(io.lines("/proc/net/arp"), "%s%s+", callback)
244 end
245
246 --- Returns conntrack information
247 -- @return Table with the currently tracked IP connections
248 function net.conntrack(callback)
249 local connt = {}
250 if fs.access("/proc/net/nf_conntrack", "r") then
251 for line in io.lines("/proc/net/nf_conntrack") do
252 line = line:match "^(.-( [^ =]+=).-)%2"
253 local entry, flags = _parse_mixed_record(line, " +")
254 entry.layer3 = flags[1]
255 entry.layer4 = flags[3]
256 for i=1, #entry do
257 entry[i] = nil
258 end
259
260 if callback then
261 callback(entry)
262 else
263 connt[#connt+1] = entry
264 end
265 end
266 elseif fs.access("/proc/net/ip_conntrack", "r") then
267 for line in io.lines("/proc/net/ip_conntrack") do
268 line = line:match "^(.-( [^ =]+=).-)%2"
269 local entry, flags = _parse_mixed_record(line, " +")
270 entry.layer3 = "ipv4"
271 entry.layer4 = flags[1]
272 for i=1, #entry do
273 entry[i] = nil
274 end
275
276 if callback then
277 callback(entry)
278 else
279 connt[#connt+1] = entry
280 end
281 end
282 else
283 return nil
284 end
285 return connt
286 end
287
288 --- Determine the current IPv4 default route. If multiple default routes exist,
289 -- return the one with the lowest metric.
290 -- @return Table with the properties of the current default route.
291 -- The following fields are defined:
292 -- { "dest", "gateway", "metric", "refcount", "usecount", "irtt",
293 -- "flags", "device" }
294 function net.defaultroute()
295 local route
296
297 net.routes(function(rt)
298 if rt.dest:prefix() == 0 and (not route or route.metric > rt.metric) then
299 route = rt
300 end
301 end)
302
303 return route
304 end
305
306 --- Determine the current IPv6 default route. If multiple default routes exist,
307 -- return the one with the lowest metric.
308 -- @return Table with the properties of the current default route.
309 -- The following fields are defined:
310 -- { "source", "dest", "nexthop", "metric", "refcount", "usecount",
311 -- "flags", "device" }
312 function net.defaultroute6()
313 local route
314
315 net.routes6(function(rt)
316 if rt.dest:prefix() == 0 and (not route or route.metric > rt.metric) then
317 route = rt
318 end
319 end)
320
321 return route
322 end
323
324 --- Determine the names of available network interfaces.
325 -- @return Table containing all current interface names
326 function net.devices()
327 local devs = {}
328 for k, v in ipairs(nixio.getifaddrs()) do
329 if v.family == "packet" then
330 devs[#devs+1] = v.name
331 end
332 end
333 return devs
334 end
335
336
337 --- Return information about available network interfaces.
338 -- @return Table containing all current interface names and their information
339 function net.deviceinfo()
340 local devs = {}
341 for k, v in ipairs(nixio.getifaddrs()) do
342 if v.family == "packet" then
343 local d = v.data
344 d[1] = d.rx_bytes
345 d[2] = d.rx_packets
346 d[3] = d.rx_errors
347 d[4] = d.rx_dropped
348 d[5] = 0
349 d[6] = 0
350 d[7] = 0
351 d[8] = d.multicast
352 d[9] = d.tx_bytes
353 d[10] = d.tx_packets
354 d[11] = d.tx_errors
355 d[12] = d.tx_dropped
356 d[13] = 0
357 d[14] = d.collisions
358 d[15] = 0
359 d[16] = 0
360 devs[v.name] = d
361 end
362 end
363 return devs
364 end
365
366
367 -- Determine the MAC address belonging to the given IP address.
368 -- @param ip IPv4 address
369 -- @return String containing the MAC address or nil if it cannot be found
370 function net.ip4mac(ip)
371 local mac = nil
372 net.arptable(function(e)
373 if e["IP address"] == ip then
374 mac = e["HW address"]
375 end
376 end)
377 return mac
378 end
379
380 --- Returns the current kernel routing table entries.
381 -- @return Table of tables with properties of the corresponding routes.
382 -- The following fields are defined for route entry tables:
383 -- { "dest", "gateway", "metric", "refcount", "usecount", "irtt",
384 -- "flags", "device" }
385 function net.routes(callback)
386 local routes = { }
387
388 for line in io.lines("/proc/net/route") do
389
390 local dev, dst_ip, gateway, flags, refcnt, usecnt, metric,
391 dst_mask, mtu, win, irtt = line:match(
392 "([^%s]+)\t([A-F0-9]+)\t([A-F0-9]+)\t([A-F0-9]+)\t" ..
393 "(%d+)\t(%d+)\t(%d+)\t([A-F0-9]+)\t(%d+)\t(%d+)\t(%d+)"
394 )
395
396 if dev then
397 gateway = luci.ip.Hex( gateway, 32, luci.ip.FAMILY_INET4 )
398 dst_mask = luci.ip.Hex( dst_mask, 32, luci.ip.FAMILY_INET4 )
399 dst_ip = luci.ip.Hex(
400 dst_ip, dst_mask:prefix(dst_mask), luci.ip.FAMILY_INET4
401 )
402
403 local rt = {
404 dest = dst_ip,
405 gateway = gateway,
406 metric = tonumber(metric),
407 refcount = tonumber(refcnt),
408 usecount = tonumber(usecnt),
409 mtu = tonumber(mtu),
410 window = tonumber(window),
411 irtt = tonumber(irtt),
412 flags = tonumber(flags, 16),
413 device = dev
414 }
415
416 if callback then
417 callback(rt)
418 else
419 routes[#routes+1] = rt
420 end
421 end
422 end
423
424 return routes
425 end
426
427 --- Returns the current ipv6 kernel routing table entries.
428 -- @return Table of tables with properties of the corresponding routes.
429 -- The following fields are defined for route entry tables:
430 -- { "source", "dest", "nexthop", "metric", "refcount", "usecount",
431 -- "flags", "device" }
432 function net.routes6(callback)
433 if fs.access("/proc/net/ipv6_route", "r") then
434 local routes = { }
435
436 for line in io.lines("/proc/net/ipv6_route") do
437
438 local dst_ip, dst_prefix, src_ip, src_prefix, nexthop,
439 metric, refcnt, usecnt, flags, dev = line:match(
440 "([a-f0-9]+) ([a-f0-9]+) " ..
441 "([a-f0-9]+) ([a-f0-9]+) " ..
442 "([a-f0-9]+) ([a-f0-9]+) " ..
443 "([a-f0-9]+) ([a-f0-9]+) " ..
444 "([a-f0-9]+) +([^%s]+)"
445 )
446
447 src_ip = luci.ip.Hex(
448 src_ip, tonumber(src_prefix, 16), luci.ip.FAMILY_INET6, false
449 )
450
451 dst_ip = luci.ip.Hex(
452 dst_ip, tonumber(dst_prefix, 16), luci.ip.FAMILY_INET6, false
453 )
454
455 nexthop = luci.ip.Hex( nexthop, 128, luci.ip.FAMILY_INET6, false )
456
457 local rt = {
458 source = src_ip,
459 dest = dst_ip,
460 nexthop = nexthop,
461 metric = tonumber(metric, 16),
462 refcount = tonumber(refcnt, 16),
463 usecount = tonumber(usecnt, 16),
464 flags = tonumber(flags, 16),
465 device = dev,
466
467 -- lua number is too small for storing the metric
468 -- add a metric_raw field with the original content
469 metric_raw = metric
470 }
471
472 if callback then
473 callback(rt)
474 else
475 routes[#routes+1] = rt
476 end
477 end
478
479 return routes
480 end
481 end
482
483 --- Tests whether the given host responds to ping probes.
484 -- @param host String containing a hostname or IPv4 address
485 -- @return Number containing 0 on success and >= 1 on error
486 function net.pingtest(host)
487 return os.execute("ping -c1 '"..host:gsub("'", '').."' >/dev/null 2>&1")
488 end
489
490
491 --- LuCI system utilities / process related functions.
492 -- @class module
493 -- @name luci.sys.process
494 process = {}
495
496 --- Get the current process id.
497 -- @class function
498 -- @name process.info
499 -- @return Number containing the current pid
500 function process.info(key)
501 local s = {uid = nixio.getuid(), gid = nixio.getgid()}
502 return not key and s or s[key]
503 end
504
505 --- Retrieve information about currently running processes.
506 -- @return Table containing process information
507 function process.list()
508 local data = {}
509 local k
510 local ps = luci.util.execi("top -bn1")
511
512 if not ps then
513 return
514 end
515
516 while true do
517 local line = ps()
518 if not line then
519 return
520 end
521
522 k = luci.util.split(luci.util.trim(line), "%s+", nil, true)
523 if k[1] == "PID" then
524 break
525 end
526 end
527
528 for line in ps do
529 local row = {}
530
531 line = luci.util.trim(line)
532 for i, value in ipairs(luci.util.split(line, "%s+", #k-1, true)) do
533 row[k[i]] = value
534 end
535
536 local pid = tonumber(row[k[1]])
537 if pid then
538 data[pid] = row
539 end
540 end
541
542 return data
543 end
544
545 --- Set the gid of a process identified by given pid.
546 -- @param gid Number containing the Unix group id
547 -- @return Boolean indicating successful operation
548 -- @return String containing the error message if failed
549 -- @return Number containing the error code if failed
550 function process.setgroup(gid)
551 return nixio.setgid(gid)
552 end
553
554 --- Set the uid of a process identified by given pid.
555 -- @param uid Number containing the Unix user id
556 -- @return Boolean indicating successful operation
557 -- @return String containing the error message if failed
558 -- @return Number containing the error code if failed
559 function process.setuser(uid)
560 return nixio.setuid(uid)
561 end
562
563 --- Send a signal to a process identified by given pid.
564 -- @class function
565 -- @name process.signal
566 -- @param pid Number containing the process id
567 -- @param sig Signal to send (default: 15 [SIGTERM])
568 -- @return Boolean indicating successful operation
569 -- @return Number containing the error code if failed
570 process.signal = nixio.kill
571
572
573 --- LuCI system utilities / user related functions.
574 -- @class module
575 -- @name luci.sys.user
576 user = {}
577
578 --- Retrieve user informations for given uid.
579 -- @class function
580 -- @name getuser
581 -- @param uid Number containing the Unix user id
582 -- @return Table containing the following fields:
583 -- { "uid", "gid", "name", "passwd", "dir", "shell", "gecos" }
584 user.getuser = nixio.getpw
585
586 --- Test whether given string matches the password of a given system user.
587 -- @param username String containing the Unix user name
588 -- @param pass String containing the password to compare
589 -- @return Boolean indicating wheather the passwords are equal
590 function user.checkpasswd(username, pass)
591 local pwe = nixio.getsp and nixio.getsp(username) or nixio.getpw(username)
592 local pwh = pwe and (pwe.pwdp or pwe.passwd)
593 if not pwh or #pwh < 1 or pwh ~= "!" and nixio.crypt(pass, pwh) ~= pwh then
594 return false
595 else
596 return true
597 end
598 end
599
600 --- Change the password of given user.
601 -- @param username String containing the Unix user name
602 -- @param password String containing the password to compare
603 -- @return Number containing 0 on success and >= 1 on error
604 function user.setpasswd(username, password)
605 if password then
606 password = password:gsub("'", "")
607 end
608
609 if username then
610 username = username:gsub("'", "")
611 end
612
613 local cmd = "(echo '"..password.."';sleep 1;echo '"..password.."')|"
614 cmd = cmd .. "passwd '"..username.."' >/dev/null 2>&1"
615 return os.execute(cmd)
616 end
617
618
619 --- LuCI system utilities / wifi related functions.
620 -- @class module
621 -- @name luci.sys.wifi
622 wifi = {}
623
624 --- Get iwconfig output for all wireless devices.
625 -- @return Table of tables containing the iwconfing output for each wifi device
626 function wifi.getiwconfig()
627 local cnt = luci.util.exec("PATH=/sbin:/usr/sbin iwconfig 2>/dev/null")
628 local iwc = {}
629
630 for i, l in pairs(luci.util.split(luci.util.trim(cnt), "\n\n")) do
631 local k = l:match("^(.-) ")
632 l = l:gsub("^(.-) +", "", 1)
633 if k then
634 local entry, flags = _parse_mixed_record(l)
635 if entry then
636 entry.flags = flags
637 end
638 iwc[k] = entry
639 end
640 end
641
642 return iwc
643 end
644
645 --- Get iwlist scan output from all wireless devices.
646 -- @return Table of tables contaiing all scan results
647 function wifi.iwscan(iface)
648 local siface = iface or ""
649 local cnt = luci.util.exec("iwlist "..siface.." scan 2>/dev/null")
650 local iws = {}
651
652 for i, l in pairs(luci.util.split(luci.util.trim(cnt), "\n\n")) do
653 local k = l:match("^(.-) ")
654 l = l:gsub("^[^\n]+", "", 1)
655 l = luci.util.trim(l)
656 if k then
657 iws[k] = {}
658 for j, c in pairs(luci.util.split(l, "\n Cell")) do
659 c = c:gsub("^(.-)- ", "", 1)
660 c = luci.util.split(c, "\n", 7)
661 c = table.concat(c, "\n", 1)
662 local entry, flags = _parse_mixed_record(c)
663 if entry then
664 entry.flags = flags
665 end
666 table.insert(iws[k], entry)
667 end
668 end
669 end
670
671 return iface and (iws[iface] or {}) or iws
672 end
673
674 --- Get available channels from given wireless iface.
675 -- @param iface Wireless interface (optional)
676 -- @return Table of available channels
677 function wifi.channels(iface)
678 local cmd = "iwlist " .. ( iface or "" ) .. " freq 2>/dev/null"
679 local cns = { }
680
681 local fd = io.popen(cmd)
682 if fd then
683 local ln, c, f
684 while true do
685 ln = fd:read("*l")
686 if not ln then break end
687 c, f = ln:match("Channel (%d+) : (%d+%.%d+) GHz")
688 if c and f then
689 cns[tonumber(c)] = tonumber(f)
690 end
691 end
692 fd:close()
693 end
694
695 if not next(cns) then
696 cns = {
697 2.412, 2.417, 2.422, 2.427, 2.432, 2.437,
698 2.442, 2.447, 2.452, 2.457, 2.462
699 }
700 end
701
702 return cns
703 end
704
705
706 --- LuCI system utilities / init related functions.
707 -- @class module
708 -- @name luci.sys.init
709 init = {}
710 init.dir = "/etc/init.d/"
711
712 --- Get the names of all installed init scripts
713 -- @return Table containing the names of all inistalled init scripts
714 function init.names()
715 local names = { }
716 for name in fs.glob(init.dir.."*") do
717 names[#names+1] = fs.basename(name)
718 end
719 return names
720 end
721
722 --- Test whether the given init script is enabled
723 -- @param name Name of the init script
724 -- @return Boolean indicating whether init is enabled
725 function init.enabled(name)
726 if fs.access(init.dir..name) then
727 return ( call(init.dir..name.." enabled") == 0 )
728 end
729 return false
730 end
731
732 --- Get the index of he given init script
733 -- @param name Name of the init script
734 -- @return Numeric index value
735 function init.index(name)
736 if fs.access(init.dir..name) then
737 return call("source "..init.dir..name.." enabled; exit $START")
738 end
739 end
740
741 --- Enable the given init script
742 -- @param name Name of the init script
743 -- @return Boolean indicating success
744 function init.enable(name)
745 if fs.access(init.dir..name) then
746 return ( call(init.dir..name.." enable") == 1 )
747 end
748 end
749
750 --- Disable the given init script
751 -- @param name Name of the init script
752 -- @return Boolean indicating success
753 function init.disable(name)
754 if fs.access(init.dir..name) then
755 return ( call(init.dir..name.." disable") == 0 )
756 end
757 end
758
759
760 -- Internal functions
761
762 function _parse_delimited_table(iter, delimiter, callback)
763 delimiter = delimiter or "%s+"
764
765 local data = {}
766 local trim = luci.util.trim
767 local split = luci.util.split
768
769 local keys = split(trim(iter()), delimiter, nil, true)
770 for i, j in pairs(keys) do
771 keys[i] = trim(keys[i])
772 end
773
774 for line in iter do
775 local row = {}
776 line = trim(line)
777 if #line > 0 then
778 for i, j in pairs(split(line, delimiter, nil, true)) do
779 if keys[i] then
780 row[keys[i]] = j
781 end
782 end
783 end
784
785 if callback then
786 callback(row)
787 else
788 data[#data+1] = row
789 end
790 end
791
792 return data
793 end
794
795 function _parse_mixed_record(cnt, delimiter)
796 delimiter = delimiter or " "
797 local data = {}
798 local flags = {}
799
800 for i, l in pairs(luci.util.split(luci.util.trim(cnt), "\n")) do
801 for j, f in pairs(luci.util.split(luci.util.trim(l), delimiter, nil, true)) do
802 local k, x, v = f:match('([^%s][^:=]*) *([:=]*) *"*([^\n"]*)"*')
803
804 if k then
805 if x == "" then
806 table.insert(flags, k)
807 else
808 data[k] = v
809 end
810 end
811 end
812 end
813
814 return data, flags
815 end