Merge pull request #464 from remakeelectric/pulls/list-operations
[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.conntrack(callback)
296         local ok, nfct = pcall(io.lines, "/proc/net/nf_conntrack")
297         if not ok or not nfct then
298                 return nil
299         end
300
301         local line, connt = nil, (not callback) and { }
302         for line in nfct do
303                 local fam, l3, l4, timeout, state, tuples =
304                         line:match("^(ipv[46]) +(%d+) +%S+ +(%d+) +(%d+) +([A-Z_]+) +(.+)$")
305
306                 if fam and l3 and l4 and timeout and state and tuples and
307                    state ~= "TIME_WAIT"
308                 then
309                         l4 = nixio.getprotobynumber(l4)
310
311                         local entry = {
312                                 bytes = 0,
313                                 packets = 0,
314                                 layer3 = fam,
315                                 layer4 = l4 and l4.name or "unknown",
316                                 timeout = tonumber(timeout, 10)
317                         }
318
319                         local key, val
320                         for key, val in tuples:gmatch("(%w+)=(%S+)") do
321                                 if key == "bytes" or key == "packets" then
322                                         entry[key] = entry[key] + tonumber(val, 10)
323                                 elseif key == "src" or key == "dst" or key == "sport" or key == "dport" then
324                                         if entry[key] == nil then
325                                                 entry[key] = val
326                                         end
327                                 elseif val then
328                                         entry[key] = val
329                                 end
330                         end
331
332                         if callback then
333                                 callback(entry)
334                         else
335                                 connt[#connt+1] = entry
336                         end
337                 end
338         end
339
340         return callback and true or connt
341 end
342
343 function net.devices()
344         local devs = {}
345         for k, v in ipairs(nixio.getifaddrs()) do
346                 if v.family == "packet" then
347                         devs[#devs+1] = v.name
348                 end
349         end
350         return devs
351 end
352
353
354 function net.deviceinfo()
355         local devs = {}
356         for k, v in ipairs(nixio.getifaddrs()) do
357                 if v.family == "packet" then
358                         local d = v.data
359                         d[1] = d.rx_bytes
360                         d[2] = d.rx_packets
361                         d[3] = d.rx_errors
362                         d[4] = d.rx_dropped
363                         d[5] = 0
364                         d[6] = 0
365                         d[7] = 0
366                         d[8] = d.multicast
367                         d[9] = d.tx_bytes
368                         d[10] = d.tx_packets
369                         d[11] = d.tx_errors
370                         d[12] = d.tx_dropped
371                         d[13] = 0
372                         d[14] = d.collisions
373                         d[15] = 0
374                         d[16] = 0
375                         devs[v.name] = d
376                 end
377         end
378         return devs
379 end
380
381
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 --                      The following fields are defined for route entry tables:
428 --                      { "source", "dest", "nexthop", "metric", "refcount", "usecount",
429 --                        "flags", "device" }
430 function net.routes6(callback)
431         if fs.access("/proc/net/ipv6_route", "r") then
432                 local routes = { }
433
434                 for line in io.lines("/proc/net/ipv6_route") do
435
436                         local dst_ip, dst_prefix, src_ip, src_prefix, nexthop,
437                                   metric, refcnt, usecnt, flags, dev = line:match(
438                                 "([a-f0-9]+) ([a-f0-9]+) " ..
439                                 "([a-f0-9]+) ([a-f0-9]+) " ..
440                                 "([a-f0-9]+) ([a-f0-9]+) " ..
441                                 "([a-f0-9]+) ([a-f0-9]+) " ..
442                                 "([a-f0-9]+) +([^%s]+)"
443                         )
444
445                         if dst_ip and dst_prefix and
446                            src_ip and src_prefix and
447                            nexthop and metric and
448                            refcnt and usecnt and
449                            flags and dev
450                         then
451                                 src_ip = luci.ip.Hex(
452                                         src_ip, tonumber(src_prefix, 16), luci.ip.FAMILY_INET6, false
453                                 )
454
455                                 dst_ip = luci.ip.Hex(
456                                         dst_ip, tonumber(dst_prefix, 16), luci.ip.FAMILY_INET6, false
457                                 )
458
459                                 nexthop = luci.ip.Hex( nexthop, 128, luci.ip.FAMILY_INET6, false )
460
461                                 local rt = {
462                                         source   = src_ip,
463                                         dest     = dst_ip,
464                                         nexthop  = nexthop,
465                                         metric   = tonumber(metric, 16),
466                                         refcount = tonumber(refcnt, 16),
467                                         usecount = tonumber(usecnt, 16),
468                                         flags    = tonumber(flags, 16),
469                                         device   = dev,
470
471                                         -- lua number is too small for storing the metric
472                                         -- add a metric_raw field with the original content
473                                         metric_raw = metric
474                                 }
475
476                                 if callback then
477                                         callback(rt)
478                                 else
479                                         routes[#routes+1] = rt
480                                 end
481                         end
482                 end
483
484                 return routes
485         end
486 end
487
488 function net.pingtest(host)
489         return os.execute("ping -c1 '"..host:gsub("'", '').."' >/dev/null 2>&1")
490 end
491
492
493 process = {}
494
495 function process.info(key)
496         local s = {uid = nixio.getuid(), gid = nixio.getgid()}
497         return not key and s or s[key]
498 end
499
500 function process.list()
501         local data = {}
502         local k
503         local ps = luci.util.execi("/bin/busybox top -bn1")
504
505         if not ps then
506                 return
507         end
508
509         for line in ps do
510                 local pid, ppid, user, stat, vsz, mem, cpu, cmd = line:match(
511                         "^ *(%d+) +(%d+) +(%S.-%S) +([RSDZTW][W ][<N ]) +(%d+) +(%d+%%) +(%d+%%) +(.+)"
512                 )
513
514                 local idx = tonumber(pid)
515                 if idx then
516                         data[idx] = {
517                                 ['PID']     = pid,
518                                 ['PPID']    = ppid,
519                                 ['USER']    = user,
520                                 ['STAT']    = stat,
521                                 ['VSZ']     = vsz,
522                                 ['%MEM']    = mem,
523                                 ['%CPU']    = cpu,
524                                 ['COMMAND'] = cmd
525                         }
526                 end
527         end
528
529         return data
530 end
531
532 function process.setgroup(gid)
533         return nixio.setgid(gid)
534 end
535
536 function process.setuser(uid)
537         return nixio.setuid(uid)
538 end
539
540 process.signal = nixio.kill
541
542
543 user = {}
544
545 --                              { "uid", "gid", "name", "passwd", "dir", "shell", "gecos" }
546 user.getuser = nixio.getpw
547
548 function user.getpasswd(username)
549         local pwe = nixio.getsp and nixio.getsp(username) or nixio.getpw(username)
550         local pwh = pwe and (pwe.pwdp or pwe.passwd)
551         if not pwh or #pwh < 1 or pwh == "!" or pwh == "x" then
552                 return nil, pwe
553         else
554                 return pwh, pwe
555         end
556 end
557
558 function user.checkpasswd(username, pass)
559         local pwh, pwe = user.getpasswd(username)
560         if pwe then
561                 return (pwh == nil or nixio.crypt(pass, pwh) == pwh)
562         end
563         return false
564 end
565
566 function user.setpasswd(username, password)
567         if password then
568                 password = password:gsub("'", [['"'"']])
569         end
570
571         if username then
572                 username = username:gsub("'", [['"'"']])
573         end
574
575         return os.execute(
576                 "(echo '" .. password .. "'; sleep 1; echo '" .. password .. "') | " ..
577                 "passwd '" .. username .. "' >/dev/null 2>&1"
578         )
579 end
580
581
582 wifi = {}
583
584 function wifi.getiwinfo(ifname)
585         local stat, iwinfo = pcall(require, "iwinfo")
586
587         if ifname then
588                 local d, n = ifname:match("^(%w+)%.network(%d+)")
589                 local wstate = luci.util.ubus("network.wireless", "status") or { }
590
591                 d = d or ifname
592                 n = n and tonumber(n) or 1
593
594                 if type(wstate[d]) == "table" and
595                    type(wstate[d].interfaces) == "table" and
596                    type(wstate[d].interfaces[n]) == "table" and
597                    type(wstate[d].interfaces[n].ifname) == "string"
598                 then
599                         ifname = wstate[d].interfaces[n].ifname
600                 else
601                         ifname = d
602                 end
603
604                 local t = stat and iwinfo.type(ifname)
605                 local x = t and iwinfo[t] or { }
606                 return setmetatable({}, {
607                         __index = function(t, k)
608                                 if k == "ifname" then
609                                         return ifname
610                                 elseif x[k] then
611                                         return x[k](ifname)
612                                 end
613                         end
614                 })
615         end
616 end
617
618
619 init = {}
620 init.dir = "/etc/init.d/"
621
622 function init.names()
623         local names = { }
624         for name in fs.glob(init.dir.."*") do
625                 names[#names+1] = fs.basename(name)
626         end
627         return names
628 end
629
630 function init.index(name)
631         if fs.access(init.dir..name) then
632                 return call("env -i sh -c 'source %s%s enabled; exit ${START:-255}' >/dev/null"
633                         %{ init.dir, name })
634         end
635 end
636
637 local function init_action(action, name)
638         if fs.access(init.dir..name) then
639                 return call("env -i %s%s %s >/dev/null" %{ init.dir, name, action })
640         end
641 end
642
643 function init.enabled(name)
644         return (init_action("enabled", name) == 0)
645 end
646
647 function init.enable(name)
648         return (init_action("enable", name) == 1)
649 end
650
651 function init.disable(name)
652         return (init_action("disable", name) == 0)
653 end
654
655 function init.start(name)
656         return (init_action("start", name) == 0)
657 end
658
659 function init.stop(name)
660         return (init_action("stop", name) == 0)
661 end