luci-base: add rpc methods for mount management
[project/luci.git] / modules / luci-base / root / usr / libexec / rpcd / luci
1 #!/usr/bin/env lua
2
3 local json = require "luci.jsonc"
4 local fs = require "nixio.fs"
5
6 local function readfile(path)
7 local s = fs.readfile(path)
8 return s and (s:gsub("^%s+", ""):gsub("%s+$", ""))
9 end
10
11 local methods = {
12 getInitList = {
13 args = { name = "name" },
14 call = function(args)
15 local sys = require "luci.sys"
16 local _, name, scripts = nil, nil, {}
17 for _, name in ipairs(args.name and { args.name } or sys.init.names()) do
18 local index = sys.init.index(name)
19 if index then
20 scripts[name] = { index = index, enabled = sys.init.enabled(name) }
21 else
22 return { error = "No such init script" }
23 end
24 end
25 return scripts
26 end
27 },
28
29 setInitAction = {
30 args = { name = "name", action = "action" },
31 call = function(args)
32 local sys = require "luci.sys"
33 if type(sys.init[args.action]) ~= "function" then
34 return { error = "Invalid action" }
35 end
36 return { result = sys.init[args.action](args.name) }
37 end
38 },
39
40 getLocaltime = {
41 call = function(args)
42 return { result = os.time() }
43 end
44 },
45
46 setLocaltime = {
47 args = { localtime = 0 },
48 call = function(args)
49 local sys = require "luci.sys"
50 local date = os.date("*t", args.localtime)
51 if date then
52 sys.call("date -s '%04d-%02d-%02d %02d:%02d:%02d' >/dev/null" %{ date.year, date.month, date.day, date.hour, date.min, date.sec })
53 sys.call("/etc/init.d/sysfixtime restart >/dev/null")
54 end
55 return { result = args.localtime }
56 end
57 },
58
59 getTimezones = {
60 call = function(args)
61 local util = require "luci.util"
62 local zones = require "luci.sys.zoneinfo"
63
64 local tz = readfile("/etc/TZ")
65 local res = util.ubus("uci", "get", {
66 config = "system",
67 section = "@system[0]",
68 option = "zonename"
69 })
70
71 local result = {}
72 local _, zone
73 for _, zone in ipairs(zones.TZ) do
74 result[zone[1]] = {
75 tzstring = zone[2],
76 active = (res and res.value == zone[1]) and true or nil
77 }
78 end
79 return result
80 end
81 },
82
83 getLEDs = {
84 call = function()
85 local iter = fs.dir("/sys/class/leds")
86 local result = { }
87
88 if iter then
89 local led
90 for led in iter do
91 local m, s
92
93 result[led] = { triggers = {} }
94
95 s = readfile("/sys/class/leds/"..led.."/trigger")
96 for s in (s or ""):gmatch("%S+") do
97 m = s:match("^%[(.+)%]$")
98 result[led].triggers[#result[led].triggers+1] = m or s
99 result[led].active_trigger = m or result[led].active_trigger
100 end
101
102 s = readfile("/sys/class/leds/"..led.."/brightness")
103 if s then
104 result[led].brightness = tonumber(s)
105 end
106
107 s = readfile("/sys/class/leds/"..led.."/max_brightness")
108 if s then
109 result[led].max_brightness = tonumber(s)
110 end
111 end
112 end
113
114 return result
115 end
116 },
117
118 getUSBDevices = {
119 call = function()
120 local fs = require "nixio.fs"
121 local iter = fs.glob("/sys/bus/usb/devices/[0-9]*/manufacturer")
122 local result = { }
123
124 if iter then
125 result.devices = {}
126
127 local p
128 for p in iter do
129 local id = p:match("/([^/]+)/manufacturer$")
130
131 result.devices[#result.devices+1] = {
132 id = id,
133 vid = readfile("/sys/bus/usb/devices/"..id.."/idVendor"),
134 pid = readfile("/sys/bus/usb/devices/"..id.."/idProduct"),
135 vendor = readfile("/sys/bus/usb/devices/"..id.."/manufacturer"),
136 product = readfile("/sys/bus/usb/devices/"..id.."/product"),
137 speed = tonumber((readfile("/sys/bus/usb/devices/"..id.."/product")))
138 }
139 end
140 end
141
142 iter = fs.glob("/sys/bus/usb/devices/*/*-port[0-9]*")
143
144 if iter then
145 result.ports = {}
146
147 local p
148 for p in iter do
149 local port = p:match("([^/]+)$")
150 local link = fs.readlink(p.."/device")
151
152 result.ports[#result.ports+1] = {
153 port = port,
154 device = link and fs.basename(link)
155 }
156 end
157 end
158
159 return result
160 end
161 },
162
163 getIfaddrs = {
164 call = function()
165 return { result = nixio.getifaddrs() }
166 end
167 },
168
169 getHostHints = {
170 call = function()
171 local sys = require "luci.sys"
172 return sys.net.host_hints()
173 end
174 },
175
176 getDUIDHints = {
177 call = function()
178 local fp = io.open('/var/hosts/odhcpd')
179 local result = { }
180 if fp then
181 for line in fp:lines() do
182 local dev, duid, name = string.match(line, '# (%S+)%s+(%S+)%s+%d+%s+(%S+)')
183 if dev and duid and name then
184 result[duid] = {
185 name = (name ~= "-") and name or nil,
186 device = dev
187 }
188 end
189 end
190 fp:close()
191 end
192 return result
193 end
194 },
195
196 getDHCPLeases = {
197 args = { family = 0 },
198 call = function(args)
199 local s = require "luci.tools.status"
200
201 if args.family == 4 then
202 return { dhcp_leases = s.dhcp_leases() }
203 elseif args.family == 6 then
204 return { dhcp6_leases = s.dhcp6_leases() }
205 else
206 return {
207 dhcp_leases = s.dhcp_leases(),
208 dhcp6_leases = s.dhcp6_leases()
209 }
210 end
211 end
212 },
213
214 getNetworkDevices = {
215 call = function(args)
216 local dir = fs.dir("/sys/class/net")
217 local result = { }
218 if dir then
219 local dev
220 for dev in dir do
221 if not result[dev] then
222 result[dev] = { name = dev }
223 end
224
225 if fs.access("/sys/class/net/"..dev.."/master") then
226 local brname = fs.basename(fs.readlink("/sys/class/net/"..dev.."/master"))
227 if not result[brname] then
228 result[brname] = { name = brname }
229 end
230
231 if not result[brname].ports then
232 result[brname].ports = { }
233 end
234
235 result[brname].ports[#result[brname].ports+1] = dev
236 elseif fs.access("/sys/class/net/"..dev.."/bridge") then
237 if not result[dev].ports then
238 result[dev].ports = { }
239 end
240
241 result[dev].id = readfile("/sys/class/net/"..dev.."/bridge/bridge_id")
242 result[dev].stp = (readfile("/sys/class/net/"..dev.."/bridge/stp_state") ~= "0")
243 result[dev].bridge = true
244 end
245
246 local opr = readfile("/sys/class/net/"..dev.."/operstate")
247
248 result[dev].up = (opr == "up" or opr == "unknown")
249 result[dev].type = tonumber(readfile("/sys/class/net/"..dev.."/type"))
250 result[dev].name = dev
251
252 local mtu = tonumber(readfile("/sys/class/net/"..dev.."/mtu"))
253 if mtu and mtu > 0 then
254 result[dev].mtu = mtu
255 end
256
257 local qlen = tonumber(readfile("/sys/class/net/"..dev.."/tx_queue_len"))
258 if qlen and qlen > 0 then
259 result[dev].qlen = qlen
260 end
261
262 local master = fs.readlink("/sys/class/net/"..dev.."/master")
263 if master then
264 result[dev].master = fs.basename(master)
265 end
266
267 local mac = readfile("/sys/class/net/"..dev.."/address")
268 if mac and #mac == 17 then
269 result[dev].mac = mac
270 end
271 end
272 end
273 return result
274 end
275 },
276
277 getWirelessDevices = {
278 call = function(args)
279 local ubus = require "ubus".connect()
280 if not ubus then
281 return { error = "Unable to establish ubus connection" }
282 end
283
284 local status = ubus:call("network.wireless", "status", {})
285 if type(status) == "table" then
286 local radioname, radiodata
287 for radioname, radiodata in pairs(status) do
288 if type(radiodata) == "table" then
289 radiodata.iwinfo = ubus:call("iwinfo", "info", { device = radioname }) or {}
290 radiodata.iwinfo.bitrate = nil
291 radiodata.iwinfo.bssid = nil
292 radiodata.iwinfo.encryption = nil
293 radiodata.iwinfo.mode = nil
294 radiodata.iwinfo.quality = nil
295 radiodata.iwinfo.quality_max = nil
296 radiodata.iwinfo.ssid = nil
297
298 local iwdata = nil
299
300 if type(radiodata.interfaces) == "table" then
301 local _, interfacedata
302 for _, interfacedata in ipairs(radiodata.interfaces) do
303 if type(interfacedata) == "table" and
304 type(interfacedata.ifname) == "string"
305 then
306 local iwinfo = ubus:call("iwinfo", "info", { device = interfacedata.ifname })
307
308 iwdata = iwdata or iwinfo
309 interfacedata.iwinfo = iwinfo or {}
310 end
311 end
312 end
313
314 radiodata.iwinfo = {}
315
316 local _, k, v
317 for k, v in pairs(iwdata or ubus:call("iwinfo", "info", { device = radioname }) or {}) do
318 if k ~= "bitrate" and k ~= "bssid" and k ~= "encryption" and
319 k ~= "mode" and k ~= "quality" and k ~= "quality_max" and
320 k ~= "ssid"
321 then
322 if type(v) == "table" then
323 radiodata.iwinfo[k] = json.parse(json.stringify(v))
324 else
325 radiodata.iwinfo[k] = v
326 end
327 end
328 end
329 end
330 end
331 end
332
333 return status
334 end
335 },
336
337 getBoardJSON = {
338 call = function(args)
339 local jsc = require "luci.jsonc"
340 return jsc.parse(fs.readfile("/etc/board.json") or "")
341 end
342 },
343
344 getConntrackHelpers = {
345 call = function()
346 local ok, fd = pcall(io.open, "/usr/share/fw3/helpers.conf", "r")
347 local rv = {}
348
349 if ok then
350 local entry
351
352 while true do
353 local line = fd:read("*l")
354 if not line then
355 break
356 end
357
358 if line:match("^%s*config%s") then
359 if entry then
360 rv[#rv+1] = entry
361 end
362 entry = {}
363 else
364 local opt, val = line:match("^%s*option%s+(%S+)%s+(%S.*)$")
365 if opt and val then
366 opt = opt:gsub("^'(.+)'$", "%1"):gsub('^"(.+)"$', "%1")
367 val = val:gsub("^'(.+)'$", "%1"):gsub('^"(.+)"$', "%1")
368 entry[opt] = val
369 end
370 end
371 end
372
373 if entry then
374 rv[#rv+1] = entry
375 end
376
377 fd:close()
378 end
379
380 return { result = rv }
381 end
382 },
383
384 getFeatures = {
385 call = function()
386 local fs = require "nixio.fs"
387 local rv = {}
388 local ok, fd
389
390 rv.firewall = fs.access("/sbin/fw3")
391 rv.opkg = fs.access("/bin/opkg")
392 rv.offloading = fs.access("/sys/module/xt_FLOWOFFLOAD/refcnt")
393 rv.br2684ctl = fs.access("/usr/sbin/br2684ctl")
394 rv.swconfig = fs.access("/sbin/swconfig")
395 rv.odhcpd = fs.access("/usr/sbin/odhcpd")
396 rv.zram = fs.access("/sys/class/zram-control")
397 rv.sysntpd = fs.readlink("/usr/sbin/ntpd") and true
398 rv.ipv6 = fs.access("/proc/net/ipv6_route")
399 rv.dropbear = fs.access("/usr/sbin/dropbear")
400
401 local wifi_features = { "eap", "11n", "11ac", "11r", "11w", "acs", "sae", "owe", "suiteb192" }
402
403 if fs.access("/usr/sbin/hostapd") then
404 rv.hostapd = { cli = fs.access("/usr/sbin/hostapd_cli") }
405
406 local _, feature
407 for _, feature in ipairs(wifi_features) do
408 rv.hostapd[feature] =
409 (os.execute(string.format("/usr/sbin/hostapd -v%s >/dev/null 2>/dev/null", feature)) == 0)
410 end
411 end
412
413 if fs.access("/usr/sbin/wpa_supplicant") then
414 rv.wpasupplicant = { cli = fs.access("/usr/sbin/wpa_cli") }
415
416 local _, feature
417 for _, feature in ipairs(wifi_features) do
418 rv.wpasupplicant[feature] =
419 (os.execute(string.format("/usr/sbin/wpa_supplicant -v%s >/dev/null 2>/dev/null", feature)) == 0)
420 end
421 end
422
423 ok, fd = pcall(io.popen, "dnsmasq --version 2>/dev/null")
424 if ok then
425 rv.dnsmasq = {}
426
427 while true do
428 local line = fd:read("*l")
429 if not line then
430 break
431 end
432
433 local opts = line:match("^Compile time options: (.+)$")
434 if opts then
435 local opt
436 for opt in opts:gmatch("%S+") do
437 local no = opt:match("^no%-(%S+)$")
438 rv.dnsmasq[string.lower(no or opt)] = not no
439 end
440 break
441 end
442 end
443
444 fd:close()
445 end
446
447 ok, fd = pcall(io.popen, "ipset --help 2>/dev/null")
448 if ok then
449 rv.ipset = {}
450
451 local sets = false
452
453 while true do
454 local line = fd:read("*l")
455 if not line then
456 break
457 elseif line:match("^Supported set types:") then
458 sets = true
459 elseif sets then
460 local set, ver = line:match("^%s+(%S+)%s+(%d+)")
461 if set and not rv.ipset[set] then
462 rv.ipset[set] = tonumber(ver)
463 end
464 end
465 end
466
467 fd:close()
468 end
469
470 return rv
471 end
472 },
473
474 getSwconfigFeatures = {
475 args = { switch = "switch0" },
476 call = function(args)
477 local util = require "luci.util"
478
479 -- Parse some common switch properties from swconfig help output.
480 local swc, err = io.popen("swconfig dev %s help 2>/dev/null" % util.shellquote(args.switch))
481 if swc then
482 local is_port_attr = false
483 local is_vlan_attr = false
484 local rv = {}
485
486 while true do
487 local line = swc:read("*l")
488 if not line then break end
489
490 if line:match("^%s+%-%-vlan") then
491 is_vlan_attr = true
492
493 elseif line:match("^%s+%-%-port") then
494 is_vlan_attr = false
495 is_port_attr = true
496
497 elseif line:match("cpu @") then
498 rv.switch_title = line:match("^switch%d: %w+%((.-)%)")
499 rv.num_vlans = tonumber(line:match("vlans: (%d+)")) or 16
500 rv.min_vid = 1
501
502 elseif line:match(": pvid") or line:match(": tag") or line:match(": vid") then
503 if is_vlan_attr then rv.vid_option = line:match(": (%w+)") end
504
505 elseif line:match(": enable_vlan4k") then
506 rv.vlan4k_option = "enable_vlan4k"
507
508 elseif line:match(": enable_vlan") then
509 rv.vlan_option = "enable_vlan"
510
511 elseif line:match(": enable_learning") then
512 rv.learning_option = "enable_learning"
513
514 elseif line:match(": enable_mirror_rx") then
515 rv.mirror_option = "enable_mirror_rx"
516
517 elseif line:match(": max_length") then
518 rv.jumbo_option = "max_length"
519 end
520 end
521
522 swc:close()
523
524 if not next(rv) then
525 return { error = "No such switch" }
526 end
527
528 return rv
529 else
530 return { error = err }
531 end
532 end
533 },
534
535 getSwconfigPortState = {
536 args = { switch = "switch0" },
537 call = function(args)
538 local util = require "luci.util"
539
540 local swc, err = io.popen("swconfig dev %s show 2>/dev/null" % util.shellquote(args.switch))
541 if swc then
542 local ports = { }
543
544 while true do
545 local line = swc:read("*l")
546 if not line then break end
547
548 local port, up = line:match("port:(%d+) link:(%w+)")
549 if port then
550 local speed = line:match(" speed:(%d+)")
551 local duplex = line:match(" (%w+)-duplex")
552 local txflow = line:match(" (txflow)")
553 local rxflow = line:match(" (rxflow)")
554 local auto = line:match(" (auto)")
555
556 ports[#ports+1] = {
557 port = tonumber(port) or 0,
558 speed = tonumber(speed) or 0,
559 link = (up == "up"),
560 duplex = (duplex == "full"),
561 rxflow = (not not rxflow),
562 txflow = (not not txflow),
563 auto = (not not auto)
564 }
565 end
566 end
567
568 swc:close()
569
570 if not next(ports) then
571 return { error = "No such switch" }
572 end
573
574 return { result = ports }
575 else
576 return { error = err }
577 end
578 end
579 },
580
581 setPassword = {
582 args = { username = "root", password = "password" },
583 call = function(args)
584 local util = require "luci.util"
585 return {
586 result = (os.execute("(echo %s; sleep 1; echo %s) | passwd %s >/dev/null 2>&1" %{
587 luci.util.shellquote(args.password),
588 luci.util.shellquote(args.password),
589 luci.util.shellquote(args.username)
590 }) == 0)
591 }
592 end
593 },
594
595 getBlockDevices = {
596 call = function()
597 local fs = require "nixio.fs"
598
599 local block = io.popen("/sbin/block info", "r")
600 if block then
601 local rv = {}
602
603 while true do
604 local ln = block:read("*l")
605 if not ln then
606 break
607 end
608
609 local dev = ln:match("^/dev/(.-):")
610 if dev then
611 local s = tonumber((fs.readfile("/sys/class/block/" .. dev .."/size")))
612 local e = {
613 dev = "/dev/" .. dev,
614 size = s and s * 512
615 }
616
617 local key, val = { }
618 for key, val in ln:gmatch([[(%w+)="(.-)"]]) do
619 e[key:lower()] = val
620 end
621
622 rv[dev] = e
623 end
624 end
625
626 block:close()
627
628 return rv
629 else
630 return { error = "Unable to execute block utility" }
631 end
632 end
633 },
634
635 setBlockDetect = {
636 call = function()
637 return { result = (os.execute("/sbin/block detect > /etc/config/fstab") == 0) }
638 end
639 },
640
641 getMountPoints = {
642 call = function()
643 local fs = require "nixio.fs"
644
645 local fd, err = io.open("/proc/mounts", "r")
646 if fd then
647 local rv = {}
648
649 while true do
650 local ln = fd:read("*l")
651 if not ln then
652 break
653 end
654
655 local device, mount, fstype, options, freq, pass = ln:match("^(%S*) (%S*) (%S*) (%S*) (%d+) (%d+)$")
656 if device and mount then
657 device = device:gsub("\\(%d+)", function(n) return string.char(tonumber(n, 8)) end)
658 mount = mount:gsub("\\(%d+)", function(n) return string.char(tonumber(n, 8)) end)
659
660 local stat = fs.statvfs(mount)
661 if stat and stat.blocks > 0 then
662 rv[#rv+1] = {
663 device = device,
664 mount = mount,
665 size = stat.bsize * stat.blocks,
666 avail = stat.bsize * stat.bavail,
667 free = stat.bsize * stat.bfree
668 }
669 end
670 end
671 end
672
673 fd:close()
674
675 return { result = rv }
676 else
677 return { error = err }
678 end
679 end
680 },
681
682 setUmount = {
683 args = { path = "/mnt" },
684 call = function(args)
685 local util = require "luci.util"
686 return { result = (os.execute(string.format("/bin/umount %s", util.shellquote(args.path))) == 0) }
687 end
688 }
689 }
690
691 local function parseInput()
692 local parse = json.new()
693 local done, err
694
695 while true do
696 local chunk = io.read(4096)
697 if not chunk then
698 break
699 elseif not done and not err then
700 done, err = parse:parse(chunk)
701 end
702 end
703
704 if not done then
705 print(json.stringify({ error = err or "Incomplete input" }))
706 os.exit(1)
707 end
708
709 return parse:get()
710 end
711
712 local function validateArgs(func, uargs)
713 local method = methods[func]
714 if not method then
715 print(json.stringify({ error = "Method not found" }))
716 os.exit(1)
717 end
718
719 if type(uargs) ~= "table" then
720 print(json.stringify({ error = "Invalid arguments" }))
721 os.exit(1)
722 end
723
724 uargs.ubus_rpc_session = nil
725
726 local k, v
727 local margs = method.args or {}
728 for k, v in pairs(uargs) do
729 if margs[k] == nil or
730 (v ~= nil and type(v) ~= type(margs[k]))
731 then
732 print(json.stringify({ error = "Invalid arguments" }))
733 os.exit(1)
734 end
735 end
736
737 return method
738 end
739
740 if arg[1] == "list" then
741 local _, method, rv = nil, nil, {}
742 for _, method in pairs(methods) do rv[_] = method.args or {} end
743 print((json.stringify(rv):gsub(":%[%]", ":{}")))
744 elseif arg[1] == "call" then
745 local args = parseInput()
746 local method = validateArgs(arg[2], args)
747 local result, code = method.call(args)
748 print((json.stringify(result):gsub("^%[%]$", "{}")))
749 os.exit(code or 0)
750 end