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