luci-base: add luci.sys.net.host_hints() and regenerate documentation
[project/luci.git] / modules / luci-base / luasrc / sys.lua
1 -- Copyright 2008 Steven Barth <steven@midlink.org>
2 -- Licensed to the public under the Apache License 2.0.
3
4 local io = require "io"
5 local os = require "os"
6 local table = require "table"
7 local nixio = require "nixio"
8 local fs = require "nixio.fs"
9 local uci = require "luci.model.uci"
10
11 local luci = {}
12 luci.util = require "luci.util"
13 luci.ip = require "luci.ip"
14
15 local tonumber, ipairs, pairs, pcall, type, next, setmetatable, require, select =
16 tonumber, ipairs, pairs, pcall, type, next, setmetatable, require, select
17
18
19 module "luci.sys"
20
21 function call(...)
22 return os.execute(...) / 256
23 end
24
25 exec = luci.util.exec
26
27 function mounts()
28 local data = {}
29 local k = {"fs", "blocks", "used", "available", "percent", "mountpoint"}
30 local ps = luci.util.execi("df")
31
32 if not ps then
33 return
34 else
35 ps()
36 end
37
38 for line in ps do
39 local row = {}
40
41 local j = 1
42 for value in line:gmatch("[^%s]+") do
43 row[k[j]] = value
44 j = j + 1
45 end
46
47 if row[k[1]] then
48
49 -- this is a rather ugly workaround to cope with wrapped lines in
50 -- the df output:
51 --
52 -- /dev/scsi/host0/bus0/target0/lun0/part3
53 -- 114382024 93566472 15005244 86% /mnt/usb
54 --
55
56 if not row[k[2]] then
57 j = 2
58 line = ps()
59 for value in line:gmatch("[^%s]+") do
60 row[k[j]] = value
61 j = j + 1
62 end
63 end
64
65 table.insert(data, row)
66 end
67 end
68
69 return data
70 end
71
72 -- containing the whole environment is returned otherwise this function returns
73 -- the corresponding string value for the given name or nil if no such variable
74 -- exists.
75 getenv = nixio.getenv
76
77 function hostname(newname)
78 if type(newname) == "string" and #newname > 0 then
79 fs.writefile( "/proc/sys/kernel/hostname", newname )
80 return newname
81 else
82 return nixio.uname().nodename
83 end
84 end
85
86 function httpget(url, stream, target)
87 if not target then
88 local source = stream and io.popen or luci.util.exec
89 return source("wget -qO- '"..url:gsub("'", "").."'")
90 else
91 return os.execute("wget -qO '%s' '%s'" %
92 {target:gsub("'", ""), url:gsub("'", "")})
93 end
94 end
95
96 function reboot()
97 return os.execute("reboot >/dev/null 2>&1")
98 end
99
100 function syslog()
101 return luci.util.exec("logread")
102 end
103
104 function dmesg()
105 return luci.util.exec("dmesg")
106 end
107
108 function uniqueid(bytes)
109 local rand = fs.readfile("/dev/urandom", bytes)
110 return rand and nixio.bin.hexlify(rand)
111 end
112
113 function uptime()
114 return nixio.sysinfo().uptime
115 end
116
117
118 net = {}
119
120 -- The following fields are defined for arp entry objects:
121 -- { "IP address", "HW address", "HW type", "Flags", "Mask", "Device" }
122 function net.arptable(callback)
123 local arp = (not callback) and {} or nil
124 local e, r, v
125 if fs.access("/proc/net/arp") then
126 for e in io.lines("/proc/net/arp") do
127 local r = { }, v
128 for v in e:gmatch("%S+") do
129 r[#r+1] = v
130 end
131
132 if r[1] ~= "IP" then
133 local x = {
134 ["IP address"] = r[1],
135 ["HW type"] = r[2],
136 ["Flags"] = r[3],
137 ["HW address"] = r[4],
138 ["Mask"] = r[5],
139 ["Device"] = r[6]
140 }
141
142 if callback then
143 callback(x)
144 else
145 arp = arp or { }
146 arp[#arp+1] = x
147 end
148 end
149 end
150 end
151 return arp
152 end
153
154 local function _nethints(what, callback)
155 local _, k, e, mac, ip, name
156 local cur = uci.cursor()
157 local ifn = { }
158 local hosts = { }
159
160 local function _add(i, ...)
161 local k = select(i, ...)
162 if k then
163 if not hosts[k] then hosts[k] = { } end
164 hosts[k][1] = select(1, ...) or hosts[k][1]
165 hosts[k][2] = select(2, ...) or hosts[k][2]
166 hosts[k][3] = select(3, ...) or hosts[k][3]
167 hosts[k][4] = select(4, ...) or hosts[k][4]
168 end
169 end
170
171 if fs.access("/proc/net/arp") then
172 for e in io.lines("/proc/net/arp") do
173 ip, mac = e:match("^([%d%.]+)%s+%S+%s+%S+%s+([a-fA-F0-9:]+)%s+")
174 if ip and mac then
175 _add(what, mac:upper(), ip, nil, nil)
176 end
177 end
178 end
179
180 if fs.access("/etc/ethers") then
181 for e in io.lines("/etc/ethers") do
182 mac, ip = e:match("^([a-f0-9]%S+) (%S+)")
183 if mac and ip then
184 _add(what, mac:upper(), ip, nil, nil)
185 end
186 end
187 end
188
189 if fs.access("/var/dhcp.leases") then
190 for e in io.lines("/var/dhcp.leases") do
191 mac, ip, name = e:match("^%d+ (%S+) (%S+) (%S+)")
192 if mac and ip then
193 _add(what, mac:upper(), ip, nil, name ~= "*" and name)
194 end
195 end
196 end
197
198 cur:foreach("dhcp", "host",
199 function(s)
200 for mac in luci.util.imatch(s.mac) do
201 _add(what, mac:upper(), s.ip, nil, s.name)
202 end
203 end)
204
205 for _, e in ipairs(nixio.getifaddrs()) do
206 if e.name ~= "lo" then
207 ifn[e.name] = ifn[e.name] or { }
208 if e.family == "packet" and e.addr and #e.addr == 17 then
209 ifn[e.name][1] = e.addr:upper()
210 elseif e.family == "inet" then
211 ifn[e.name][2] = e.addr
212 elseif e.family == "inet6" then
213 ifn[e.name][3] = e.addr
214 end
215 end
216 end
217
218 for _, e in pairs(ifn) do
219 if e[what] and (e[2] or e[3]) then
220 _add(what, e[1], e[2], e[3], e[4])
221 end
222 end
223
224 for _, e in luci.util.kspairs(hosts) do
225 callback(e[1], e[2], e[3], e[4])
226 end
227 end
228
229 -- Each entry contains the values in the following order:
230 -- [ "mac", "name" ]
231 function net.mac_hints(callback)
232 if callback then
233 _nethints(1, function(mac, v4, v6, name)
234 name = name or nixio.getnameinfo(v4 or v6, nil, 100) or v4
235 if name and name ~= mac then
236 callback(mac, name or nixio.getnameinfo(v4 or v6, nil, 100) or v4)
237 end
238 end)
239 else
240 local rv = { }
241 _nethints(1, function(mac, v4, v6, name)
242 name = name or nixio.getnameinfo(v4 or v6, nil, 100) or v4
243 if name and name ~= mac then
244 rv[#rv+1] = { mac, name or nixio.getnameinfo(v4 or v6, nil, 100) or v4 }
245 end
246 end)
247 return rv
248 end
249 end
250
251 -- Each entry contains the values in the following order:
252 -- [ "ip", "name" ]
253 function net.ipv4_hints(callback)
254 if callback then
255 _nethints(2, function(mac, v4, v6, name)
256 name = name or nixio.getnameinfo(v4, nil, 100) or mac
257 if name and name ~= v4 then
258 callback(v4, name)
259 end
260 end)
261 else
262 local rv = { }
263 _nethints(2, function(mac, v4, v6, name)
264 name = name or nixio.getnameinfo(v4, nil, 100) or mac
265 if name and name ~= v4 then
266 rv[#rv+1] = { v4, name }
267 end
268 end)
269 return rv
270 end
271 end
272
273 -- Each entry contains the values in the following order:
274 -- [ "ip", "name" ]
275 function net.ipv6_hints(callback)
276 if callback then
277 _nethints(3, function(mac, v4, v6, name)
278 name = name or nixio.getnameinfo(v6, nil, 100) or mac
279 if name and name ~= v6 then
280 callback(v6, name)
281 end
282 end)
283 else
284 local rv = { }
285 _nethints(3, function(mac, v4, v6, name)
286 name = name or nixio.getnameinfo(v6, nil, 100) or mac
287 if name and name ~= v6 then
288 rv[#rv+1] = { v6, name }
289 end
290 end)
291 return rv
292 end
293 end
294
295 function net.host_hints(callback)
296 if callback then
297 _nethints(1, function(mac, v4, v6, name)
298 if mac and mac ~= "00:00:00:00:00:00" and (v4 or v6 or name) then
299 callback(mac, v4, v6, name)
300 end
301 end)
302 else
303 local rv = { }
304 _nethints(1, function(mac, v4, v6, name)
305 if mac and mac ~= "00:00:00:00:00:00" and (v4 or v6 or name) then
306 local e = { }
307 if v4 then e.ipv4 = v4 end
308 if v6 then e.ipv6 = v6 end
309 if name then e.name = name end
310 rv[mac] = e
311 end
312 end)
313 return rv
314 end
315 end
316
317 function net.conntrack(callback)
318 local ok, nfct = pcall(io.lines, "/proc/net/nf_conntrack")
319 if not ok or not nfct then
320 return nil
321 end
322
323 local line, connt = nil, (not callback) and { }
324 for line in nfct do
325 local fam, l3, l4, timeout, state, tuples =
326 line:match("^(ipv[46]) +(%d+) +%S+ +(%d+) +(%d+) +([A-Z_]+) +(.+)$")
327
328 if fam and l3 and l4 and timeout and state and tuples and
329 state ~= "TIME_WAIT"
330 then
331 l4 = nixio.getprotobynumber(l4)
332
333 local entry = {
334 bytes = 0,
335 packets = 0,
336 layer3 = fam,
337 layer4 = l4 and l4.name or "unknown",
338 timeout = tonumber(timeout, 10)
339 }
340
341 local key, val
342 for key, val in tuples:gmatch("(%w+)=(%S+)") do
343 if key == "bytes" or key == "packets" then
344 entry[key] = entry[key] + tonumber(val, 10)
345 elseif key == "src" or key == "dst" or key == "sport" or key == "dport" then
346 if entry[key] == nil then
347 entry[key] = val
348 end
349 elseif val then
350 entry[key] = val
351 end
352 end
353
354 if callback then
355 callback(entry)
356 else
357 connt[#connt+1] = entry
358 end
359 end
360 end
361
362 return callback and true or connt
363 end
364
365 function net.devices()
366 local devs = {}
367 for k, v in ipairs(nixio.getifaddrs()) do
368 if v.family == "packet" then
369 devs[#devs+1] = v.name
370 end
371 end
372 return devs
373 end
374
375
376 function net.deviceinfo()
377 local devs = {}
378 for k, v in ipairs(nixio.getifaddrs()) do
379 if v.family == "packet" then
380 local d = v.data
381 d[1] = d.rx_bytes
382 d[2] = d.rx_packets
383 d[3] = d.rx_errors
384 d[4] = d.rx_dropped
385 d[5] = 0
386 d[6] = 0
387 d[7] = 0
388 d[8] = d.multicast
389 d[9] = d.tx_bytes
390 d[10] = d.tx_packets
391 d[11] = d.tx_errors
392 d[12] = d.tx_dropped
393 d[13] = 0
394 d[14] = d.collisions
395 d[15] = 0
396 d[16] = 0
397 devs[v.name] = d
398 end
399 end
400 return devs
401 end
402
403
404 -- The following fields are defined for route entry tables:
405 -- { "dest", "gateway", "metric", "refcount", "usecount", "irtt",
406 -- "flags", "device" }
407 function net.routes(callback)
408 local routes = { }
409
410 for line in io.lines("/proc/net/route") do
411
412 local dev, dst_ip, gateway, flags, refcnt, usecnt, metric,
413 dst_mask, mtu, win, irtt = line:match(
414 "([^%s]+)\t([A-F0-9]+)\t([A-F0-9]+)\t([A-F0-9]+)\t" ..
415 "(%d+)\t(%d+)\t(%d+)\t([A-F0-9]+)\t(%d+)\t(%d+)\t(%d+)"
416 )
417
418 if dev then
419 gateway = luci.ip.Hex( gateway, 32, luci.ip.FAMILY_INET4 )
420 dst_mask = luci.ip.Hex( dst_mask, 32, luci.ip.FAMILY_INET4 )
421 dst_ip = luci.ip.Hex(
422 dst_ip, dst_mask:prefix(dst_mask), luci.ip.FAMILY_INET4
423 )
424
425 local rt = {
426 dest = dst_ip,
427 gateway = gateway,
428 metric = tonumber(metric),
429 refcount = tonumber(refcnt),
430 usecount = tonumber(usecnt),
431 mtu = tonumber(mtu),
432 window = tonumber(window),
433 irtt = tonumber(irtt),
434 flags = tonumber(flags, 16),
435 device = dev
436 }
437
438 if callback then
439 callback(rt)
440 else
441 routes[#routes+1] = rt
442 end
443 end
444 end
445
446 return routes
447 end
448
449 -- The following fields are defined for route entry tables:
450 -- { "source", "dest", "nexthop", "metric", "refcount", "usecount",
451 -- "flags", "device" }
452 function net.routes6(callback)
453 if fs.access("/proc/net/ipv6_route", "r") then
454 local routes = { }
455
456 for line in io.lines("/proc/net/ipv6_route") do
457
458 local dst_ip, dst_prefix, src_ip, src_prefix, nexthop,
459 metric, refcnt, usecnt, flags, dev = line:match(
460 "([a-f0-9]+) ([a-f0-9]+) " ..
461 "([a-f0-9]+) ([a-f0-9]+) " ..
462 "([a-f0-9]+) ([a-f0-9]+) " ..
463 "([a-f0-9]+) ([a-f0-9]+) " ..
464 "([a-f0-9]+) +([^%s]+)"
465 )
466
467 if dst_ip and dst_prefix and
468 src_ip and src_prefix and
469 nexthop and metric and
470 refcnt and usecnt and
471 flags and dev
472 then
473 src_ip = luci.ip.Hex(
474 src_ip, tonumber(src_prefix, 16), luci.ip.FAMILY_INET6, false
475 )
476
477 dst_ip = luci.ip.Hex(
478 dst_ip, tonumber(dst_prefix, 16), luci.ip.FAMILY_INET6, false
479 )
480
481 nexthop = luci.ip.Hex( nexthop, 128, luci.ip.FAMILY_INET6, false )
482
483 local rt = {
484 source = src_ip,
485 dest = dst_ip,
486 nexthop = nexthop,
487 metric = tonumber(metric, 16),
488 refcount = tonumber(refcnt, 16),
489 usecount = tonumber(usecnt, 16),
490 flags = tonumber(flags, 16),
491 device = dev,
492
493 -- lua number is too small for storing the metric
494 -- add a metric_raw field with the original content
495 metric_raw = metric
496 }
497
498 if callback then
499 callback(rt)
500 else
501 routes[#routes+1] = rt
502 end
503 end
504 end
505
506 return routes
507 end
508 end
509
510 function net.pingtest(host)
511 return os.execute("ping -c1 '"..host:gsub("'", '').."' >/dev/null 2>&1")
512 end
513
514
515 process = {}
516
517 function process.info(key)
518 local s = {uid = nixio.getuid(), gid = nixio.getgid()}
519 return not key and s or s[key]
520 end
521
522 function process.list()
523 local data = {}
524 local k
525 local ps = luci.util.execi("/bin/busybox top -bn1")
526
527 if not ps then
528 return
529 end
530
531 for line in ps do
532 local pid, ppid, user, stat, vsz, mem, cpu, cmd = line:match(
533 "^ *(%d+) +(%d+) +(%S.-%S) +([RSDZTW][W ][<N ]) +(%d+) +(%d+%%) +(%d+%%) +(.+)"
534 )
535
536 local idx = tonumber(pid)
537 if idx then
538 data[idx] = {
539 ['PID'] = pid,
540 ['PPID'] = ppid,
541 ['USER'] = user,
542 ['STAT'] = stat,
543 ['VSZ'] = vsz,
544 ['%MEM'] = mem,
545 ['%CPU'] = cpu,
546 ['COMMAND'] = cmd
547 }
548 end
549 end
550
551 return data
552 end
553
554 function process.setgroup(gid)
555 return nixio.setgid(gid)
556 end
557
558 function process.setuser(uid)
559 return nixio.setuid(uid)
560 end
561
562 process.signal = nixio.kill
563
564
565 user = {}
566
567 -- { "uid", "gid", "name", "passwd", "dir", "shell", "gecos" }
568 user.getuser = nixio.getpw
569
570 function user.getpasswd(username)
571 local pwe = nixio.getsp and nixio.getsp(username) or nixio.getpw(username)
572 local pwh = pwe and (pwe.pwdp or pwe.passwd)
573 if not pwh or #pwh < 1 or pwh == "!" or pwh == "x" then
574 return nil, pwe
575 else
576 return pwh, pwe
577 end
578 end
579
580 function user.checkpasswd(username, pass)
581 local pwh, pwe = user.getpasswd(username)
582 if pwe then
583 return (pwh == nil or nixio.crypt(pass, pwh) == pwh)
584 end
585 return false
586 end
587
588 function user.setpasswd(username, password)
589 if password then
590 password = password:gsub("'", [['"'"']])
591 end
592
593 if username then
594 username = username:gsub("'", [['"'"']])
595 end
596
597 return os.execute(
598 "(echo '" .. password .. "'; sleep 1; echo '" .. password .. "') | " ..
599 "passwd '" .. username .. "' >/dev/null 2>&1"
600 )
601 end
602
603
604 wifi = {}
605
606 function wifi.getiwinfo(ifname)
607 local stat, iwinfo = pcall(require, "iwinfo")
608
609 if ifname then
610 local d, n = ifname:match("^(%w+)%.network(%d+)")
611 local wstate = luci.util.ubus("network.wireless", "status") or { }
612
613 d = d or ifname
614 n = n and tonumber(n) or 1
615
616 if type(wstate[d]) == "table" and
617 type(wstate[d].interfaces) == "table" and
618 type(wstate[d].interfaces[n]) == "table" and
619 type(wstate[d].interfaces[n].ifname) == "string"
620 then
621 ifname = wstate[d].interfaces[n].ifname
622 else
623 ifname = d
624 end
625
626 local t = stat and iwinfo.type(ifname)
627 local x = t and iwinfo[t] or { }
628 return setmetatable({}, {
629 __index = function(t, k)
630 if k == "ifname" then
631 return ifname
632 elseif x[k] then
633 return x[k](ifname)
634 end
635 end
636 })
637 end
638 end
639
640
641 init = {}
642 init.dir = "/etc/init.d/"
643
644 function init.names()
645 local names = { }
646 for name in fs.glob(init.dir.."*") do
647 names[#names+1] = fs.basename(name)
648 end
649 return names
650 end
651
652 function init.index(name)
653 if fs.access(init.dir..name) then
654 return call("env -i sh -c 'source %s%s enabled; exit ${START:-255}' >/dev/null"
655 %{ init.dir, name })
656 end
657 end
658
659 local function init_action(action, name)
660 if fs.access(init.dir..name) then
661 return call("env -i %s%s %s >/dev/null" %{ init.dir, name, action })
662 end
663 end
664
665 function init.enabled(name)
666 return (init_action("enabled", name) == 0)
667 end
668
669 function init.enable(name)
670 return (init_action("enable", name) == 1)
671 end
672
673 function init.disable(name)
674 return (init_action("disable", name) == 0)
675 end
676
677 function init.start(name)
678 return (init_action("start", name) == 0)
679 end
680
681 function init.stop(name)
682 return (init_action("stop", name) == 0)
683 end