luci-base: rework dynamic list template
[oweals/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 local ntm    = require "luci.model.network"
11
12 local luci  = {}
13 luci.util   = require "luci.util"
14 luci.ip     = require "luci.ip"
15
16 local tonumber, ipairs, pairs, pcall, type, next, setmetatable, require, select =
17         tonumber, ipairs, pairs, pcall, type, next, setmetatable, require, select
18
19
20 module "luci.sys"
21
22 function call(...)
23         return os.execute(...) / 256
24 end
25
26 exec = luci.util.exec
27
28 function mounts()
29         local data = {}
30         local k = {"fs", "blocks", "used", "available", "percent", "mountpoint"}
31         local ps = luci.util.execi("df")
32
33         if not ps then
34                 return
35         else
36                 ps()
37         end
38
39         for line in ps do
40                 local row = {}
41
42                 local j = 1
43                 for value in line:gmatch("[^%s]+") do
44                         row[k[j]] = value
45                         j = j + 1
46                 end
47
48                 if row[k[1]] then
49
50                         -- this is a rather ugly workaround to cope with wrapped lines in
51                         -- the df output:
52                         --
53                         --      /dev/scsi/host0/bus0/target0/lun0/part3
54                         --                   114382024  93566472  15005244  86% /mnt/usb
55                         --
56
57                         if not row[k[2]] then
58                                 j = 2
59                                 line = ps()
60                                 for value in line:gmatch("[^%s]+") do
61                                         row[k[j]] = value
62                                         j = j + 1
63                                 end
64                         end
65
66                         table.insert(data, row)
67                 end
68         end
69
70         return data
71 end
72
73 function mtds()
74         local data = {}
75
76         if fs.access("/proc/mtd") then
77                 for l in io.lines("/proc/mtd") do
78                         local d, s, e, n = l:match('^([^%s]+)%s+([^%s]+)%s+([^%s]+)%s+"([^%s]+)"')
79                         if s and n then
80                                 local d = {}
81                                 d.size = tonumber(s, 16)
82                                 d.name = n
83                                 table.insert(data, d)
84                         end
85                 end
86         end
87
88         return data
89 end
90
91 -- containing the whole environment is returned otherwise this function returns
92 -- the corresponding string value for the given name or nil if no such variable
93 -- exists.
94 getenv = nixio.getenv
95
96 function hostname(newname)
97         if type(newname) == "string" and #newname > 0 then
98                 fs.writefile( "/proc/sys/kernel/hostname", newname )
99                 return newname
100         else
101                 return nixio.uname().nodename
102         end
103 end
104
105 function httpget(url, stream, target)
106         if not target then
107                 local source = stream and io.popen or luci.util.exec
108                 return source("wget -qO- %s" % luci.util.shellquote(url))
109         else
110                 return os.execute("wget -qO %s %s" %
111                         {luci.util.shellquote(target), luci.util.shellquote(url)})
112         end
113 end
114
115 function reboot()
116         return os.execute("reboot >/dev/null 2>&1")
117 end
118
119 function syslog()
120         return luci.util.exec("logread")
121 end
122
123 function dmesg()
124         return luci.util.exec("dmesg")
125 end
126
127 function uniqueid(bytes)
128         local rand = fs.readfile("/dev/urandom", bytes)
129         return rand and nixio.bin.hexlify(rand)
130 end
131
132 function uptime()
133         return nixio.sysinfo().uptime
134 end
135
136
137 net = {}
138
139 local function _nethints(what, callback)
140         local _, k, e, mac, ip, name
141         local cur = uci.cursor()
142         local ifn = { }
143         local hosts = { }
144         local lookup = { }
145
146         local function _add(i, ...)
147                 local k = select(i, ...)
148                 if k then
149                         if not hosts[k] then hosts[k] = { } end
150                         hosts[k][1] = select(1, ...) or hosts[k][1]
151                         hosts[k][2] = select(2, ...) or hosts[k][2]
152                         hosts[k][3] = select(3, ...) or hosts[k][3]
153                         hosts[k][4] = select(4, ...) or hosts[k][4]
154                 end
155         end
156
157         luci.ip.neighbors(nil, function(neigh)
158                 if neigh.mac and neigh.family == 4 then
159                         _add(what, neigh.mac:string(), neigh.dest:string(), nil, nil)
160                 elseif neigh.mac and neigh.family == 6 then
161                         _add(what, neigh.mac:string(), nil, neigh.dest:string(), nil)
162                 end
163         end)
164
165         if fs.access("/etc/ethers") then
166                 for e in io.lines("/etc/ethers") do
167                         mac, name = e:match("^([a-fA-F0-9:-]+)%s+(%S+)")
168                         mac = luci.ip.checkmac(mac)
169                         if mac and name then
170                                 if luci.ip.checkip4(name) then
171                                         _add(what, mac, name, nil, nil)
172                                 else
173                                         _add(what, mac, nil, nil, name)
174                                 end
175                         end
176                 end
177         end
178
179         cur:foreach("dhcp", "dnsmasq",
180                 function(s)
181                         if s.leasefile and fs.access(s.leasefile) then
182                                 for e in io.lines(s.leasefile) do
183                                         mac, ip, name = e:match("^%d+ (%S+) (%S+) (%S+)")
184                                         mac = luci.ip.checkmac(mac)
185                                         if mac and ip then
186                                                 _add(what, mac, ip, nil, name ~= "*" and name)
187                                         end
188                                 end
189                         end
190                 end
191         )
192
193         cur:foreach("dhcp", "host",
194                 function(s)
195                         for mac in luci.util.imatch(s.mac) do
196                                 mac = luci.ip.checkmac(mac)
197                                 if mac then
198                                         _add(what, mac, s.ip, nil, s.name)
199                                 end
200                         end
201                 end)
202
203         for _, e in ipairs(nixio.getifaddrs()) do
204                 if e.name ~= "lo" then
205                         ifn[e.name] = ifn[e.name] or { }
206                         if e.family == "packet" and e.addr and #e.addr == 17 then
207                                 ifn[e.name][1] = e.addr:upper()
208                         elseif e.family == "inet" then
209                                 ifn[e.name][2] = e.addr
210                         elseif e.family == "inet6" then
211                                 ifn[e.name][3] = e.addr
212                         end
213                 end
214         end
215
216         for _, e in pairs(ifn) do
217                 if e[what] and (e[2] or e[3]) then
218                         _add(what, e[1], e[2], e[3], e[4])
219                 end
220         end
221
222         for _, e in pairs(hosts) do
223                 lookup[#lookup+1] = (what > 1) and e[what] or (e[2] or e[3])
224         end
225
226         if #lookup > 0 then
227                 lookup = luci.util.ubus("network.rrdns", "lookup", {
228                         addrs   = lookup,
229                         timeout = 250,
230                         limit   = 1000
231                 }) or { }
232         end
233
234         for _, e in luci.util.kspairs(hosts) do
235                 callback(e[1], e[2], e[3], lookup[e[2]] or lookup[e[3]] or e[4])
236         end
237 end
238
239 --          Each entry contains the values in the following order:
240 --          [ "mac", "name" ]
241 function net.mac_hints(callback)
242         if callback then
243                 _nethints(1, function(mac, v4, v6, name)
244                         name = name or v4
245                         if name and name ~= mac then
246                                 callback(mac, name or v4)
247                         end
248                 end)
249         else
250                 local rv = { }
251                 _nethints(1, function(mac, v4, v6, name)
252                         name = name or v4
253                         if name and name ~= mac then
254                                 rv[#rv+1] = { mac, name or v4 }
255                         end
256                 end)
257                 return rv
258         end
259 end
260
261 --          Each entry contains the values in the following order:
262 --          [ "ip", "name" ]
263 function net.ipv4_hints(callback)
264         if callback then
265                 _nethints(2, function(mac, v4, v6, name)
266                         name = name or mac
267                         if name and name ~= v4 then
268                                 callback(v4, name)
269                         end
270                 end)
271         else
272                 local rv = { }
273                 _nethints(2, function(mac, v4, v6, name)
274                         name = name or mac
275                         if name and name ~= v4 then
276                                 rv[#rv+1] = { v4, name }
277                         end
278                 end)
279                 return rv
280         end
281 end
282
283 --          Each entry contains the values in the following order:
284 --          [ "ip", "name" ]
285 function net.ipv6_hints(callback)
286         if callback then
287                 _nethints(3, function(mac, v4, v6, name)
288                         name = name or mac
289                         if name and name ~= v6 then
290                                 callback(v6, name)
291                         end
292                 end)
293         else
294                 local rv = { }
295                 _nethints(3, function(mac, v4, v6, name)
296                         name = name or mac
297                         if name and name ~= v6 then
298                                 rv[#rv+1] = { v6, name }
299                         end
300                 end)
301                 return rv
302         end
303 end
304
305 function net.host_hints(callback)
306         if callback then
307                 _nethints(1, function(mac, v4, v6, name)
308                         if mac and mac ~= "00:00:00:00:00:00" and (v4 or v6 or name) then
309                                 callback(mac, v4, v6, name)
310                         end
311                 end)
312         else
313                 local rv = { }
314                 _nethints(1, function(mac, v4, v6, name)
315                         if mac and mac ~= "00:00:00:00:00:00" and (v4 or v6 or name) then
316                                 local e = { }
317                                 if v4   then e.ipv4 = v4   end
318                                 if v6   then e.ipv6 = v6   end
319                                 if name then e.name = name end
320                                 rv[mac] = e
321                         end
322                 end)
323                 return rv
324         end
325 end
326
327 function net.conntrack(callback)
328         local ok, nfct = pcall(io.lines, "/proc/net/nf_conntrack")
329         if not ok or not nfct then
330                 return nil
331         end
332
333         local line, connt = nil, (not callback) and { }
334         for line in nfct do
335                 local fam, l3, l4, timeout, tuples =
336                         line:match("^(ipv[46]) +(%d+) +%S+ +(%d+) +(%d+) +(.+)$")
337
338                 if fam and l3 and l4 and timeout and not tuples:match("^TIME_WAIT ") then
339                         l4 = nixio.getprotobynumber(l4)
340
341                         local entry = {
342                                 bytes = 0,
343                                 packets = 0,
344                                 layer3 = fam,
345                                 layer4 = l4 and l4.name or "unknown",
346                                 timeout = tonumber(timeout, 10)
347                         }
348
349                         local key, val
350                         for key, val in tuples:gmatch("(%w+)=(%S+)") do
351                                 if key == "bytes" or key == "packets" then
352                                         entry[key] = entry[key] + tonumber(val, 10)
353                                 elseif key == "src" or key == "dst" then
354                                         if entry[key] == nil then
355                                                 entry[key] = luci.ip.new(val):string()
356                                         end
357                                 elseif key == "sport" or key == "dport" then
358                                         if entry[key] == nil then
359                                                 entry[key] = val
360                                         end
361                                 elseif val then
362                                         entry[key] = val
363                                 end
364                         end
365
366                         if callback then
367                                 callback(entry)
368                         else
369                                 connt[#connt+1] = entry
370                         end
371                 end
372         end
373
374         return callback and true or connt
375 end
376
377 function net.devices()
378         local devs = {}
379         local seen = {}
380         for k, v in ipairs(nixio.getifaddrs()) do
381                 if v.name and not seen[v.name] then
382                         seen[v.name] = true
383                         devs[#devs+1] = v.name
384                 end
385         end
386         return devs
387 end
388
389
390 process = {}
391
392 function process.info(key)
393         local s = {uid = nixio.getuid(), gid = nixio.getgid()}
394         return not key and s or s[key]
395 end
396
397 function process.list()
398         local data = {}
399         local k
400         local ps = luci.util.execi("/bin/busybox top -bn1")
401
402         if not ps then
403                 return
404         end
405
406         for line in ps do
407                 local pid, ppid, user, stat, vsz, mem, cpu, cmd = line:match(
408                         "^ *(%d+) +(%d+) +(%S.-%S) +([RSDZTW][W ][<N ]) +(%d+) +(%d+%%) +(%d+%%) +(.+)"
409                 )
410
411                 local idx = tonumber(pid)
412                 if idx then
413                         data[idx] = {
414                                 ['PID']     = pid,
415                                 ['PPID']    = ppid,
416                                 ['USER']    = user,
417                                 ['STAT']    = stat,
418                                 ['VSZ']     = vsz,
419                                 ['%MEM']    = mem,
420                                 ['%CPU']    = cpu,
421                                 ['COMMAND'] = cmd
422                         }
423                 end
424         end
425
426         return data
427 end
428
429 function process.setgroup(gid)
430         return nixio.setgid(gid)
431 end
432
433 function process.setuser(uid)
434         return nixio.setuid(uid)
435 end
436
437 process.signal = nixio.kill
438
439
440 user = {}
441
442 --                              { "uid", "gid", "name", "passwd", "dir", "shell", "gecos" }
443 user.getuser = nixio.getpw
444
445 function user.getpasswd(username)
446         local pwe = nixio.getsp and nixio.getsp(username) or nixio.getpw(username)
447         local pwh = pwe and (pwe.pwdp or pwe.passwd)
448         if not pwh or #pwh < 1 or pwh == "!" or pwh == "x" then
449                 return nil, pwe
450         else
451                 return pwh, pwe
452         end
453 end
454
455 function user.checkpasswd(username, pass)
456         local pwh, pwe = user.getpasswd(username)
457         if pwe then
458                 return (pwh == nil or nixio.crypt(pass, pwh) == pwh)
459         end
460         return false
461 end
462
463 function user.setpasswd(username, password)
464         return os.execute("(echo %s; sleep 1; echo %s) | passwd %s >/dev/null 2>&1" %{
465                 luci.util.shellquote(password),
466                 luci.util.shellquote(password),
467                 luci.util.shellquote(username)
468         })
469 end
470
471
472 wifi = {}
473
474 function wifi.getiwinfo(ifname)
475         ntm.init()
476
477         local wnet = ntm:get_wifinet(ifname)
478         if wnet and wnet.iwinfo then
479                 return wnet.iwinfo
480         end
481
482         local wdev = ntm:get_wifidev(ifname)
483         if wdev and wdev.iwinfo then
484                 return wdev.iwinfo
485         end
486
487         return { ifname = ifname }
488 end
489
490
491 init = {}
492 init.dir = "/etc/init.d/"
493
494 function init.names()
495         local names = { }
496         for name in fs.glob(init.dir.."*") do
497                 names[#names+1] = fs.basename(name)
498         end
499         return names
500 end
501
502 function init.index(name)
503         if fs.access(init.dir..name) then
504                 return call("env -i sh -c 'source %s%s enabled; exit ${START:-255}' >/dev/null"
505                         %{ init.dir, name })
506         end
507 end
508
509 local function init_action(action, name)
510         if fs.access(init.dir..name) then
511                 return call("env -i %s%s %s >/dev/null" %{ init.dir, name, action })
512         end
513 end
514
515 function init.enabled(name)
516         return (init_action("enabled", name) == 0)
517 end
518
519 function init.enable(name)
520         return (init_action("enable", name) == 1)
521 end
522
523 function init.disable(name)
524         return (init_action("disable", name) == 0)
525 end
526
527 function init.start(name)
528         return (init_action("start", name) == 0)
529 end
530
531 function init.stop(name)
532         return (init_action("stop", name) == 0)
533 end