libs/sys: implement luci.sys.wifi.getiwinfo() oop interface
[project/luci.git] / libs / sys / luasrc / sys.lua
1 --[[
2 LuCI - System library
3
4 Description:
5 Utilities for interaction with the Linux system
6
7 FileId:
8 $Id$
9
10 License:
11 Copyright 2008 Steven Barth <steven@midlink.org>
12
13 Licensed under the Apache License, Version 2.0 (the "License");
14 you may not use this file except in compliance with the License.
15 You may obtain a copy of the License at
16
17         http://www.apache.org/licenses/LICENSE-2.0
18
19 Unless required by applicable law or agreed to in writing, software
20 distributed under the License is distributed on an "AS IS" BASIS,
21 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
22 See the License for the specific language governing permissions and
23 limitations under the License.
24
25 ]]--
26
27
28 local io     = require "io"
29 local os     = require "os"
30 local table  = require "table"
31 local nixio  = require "nixio"
32 local fs     = require "nixio.fs"
33 local iwinfo = require "iwinfo"
34
35 local luci  = {}
36 luci.util   = require "luci.util"
37 luci.ip     = require "luci.ip"
38
39 local tonumber, ipairs, pairs, pcall, type, next, setmetatable =
40         tonumber, ipairs, pairs, pcall, type, next, setmetatable
41
42
43 --- LuCI Linux and POSIX system utilities.
44 module "luci.sys"
45
46 --- Execute a given shell command and return the error code
47 -- @class               function
48 -- @name                call
49 -- @param               ...             Command to call
50 -- @return              Error code of the command
51 function call(...)
52         return os.execute(...) / 256
53 end
54
55 --- Execute a given shell command and capture its standard output
56 -- @class               function
57 -- @name                exec
58 -- @param command       Command to call
59 -- @return                      String containg the return the output of the command
60 exec = luci.util.exec
61
62 --- Invoke the luci-flash executable to write an image to the flash memory.
63 -- @param image         Local path or URL to image file
64 -- @param kpattern      Pattern of files to keep over flash process
65 -- @return                      Return value of os.execute()
66 function flash(image, kpattern)
67         local cmd = "luci-flash "
68         if kpattern then
69                 cmd = cmd .. "-k '" .. kpattern:gsub("'", "") .. "' "
70         end
71         cmd = cmd .. "'" .. image:gsub("'", "") .. "' >/dev/null 2>&1"
72
73         return os.execute(cmd)
74 end
75
76 --- Retrieve information about currently mounted file systems.
77 -- @return      Table containing mount information
78 function mounts()
79         local data = {}
80         local k = {"fs", "blocks", "used", "available", "percent", "mountpoint"}
81         local ps = luci.util.execi("df")
82
83         if not ps then
84                 return
85         else
86                 ps()
87         end
88
89         for line in ps do
90                 local row = {}
91
92                 local j = 1
93                 for value in line:gmatch("[^%s]+") do
94                         row[k[j]] = value
95                         j = j + 1
96                 end
97
98                 if row[k[1]] then
99
100                         -- this is a rather ugly workaround to cope with wrapped lines in
101                         -- the df output:
102                         --
103                         --      /dev/scsi/host0/bus0/target0/lun0/part3
104                         --                   114382024  93566472  15005244  86% /mnt/usb
105                         --
106
107                         if not row[k[2]] then
108                                 j = 2
109                                 line = ps()
110                                 for value in line:gmatch("[^%s]+") do
111                                         row[k[j]] = value
112                                         j = j + 1
113                                 end
114                         end
115
116                         table.insert(data, row)
117                 end
118         end
119
120         return data
121 end
122
123 --- Retrieve environment variables. If no variable is given then a table
124 -- containing the whole environment is returned otherwise this function returns
125 -- the corresponding string value for the given name or nil if no such variable
126 -- exists.
127 -- @class               function
128 -- @name                getenv
129 -- @param var   Name of the environment variable to retrieve (optional)
130 -- @return              String containg the value of the specified variable
131 -- @return              Table containing all variables if no variable name is given
132 getenv = nixio.getenv
133
134 --- Get or set the current hostname.
135 -- @param               String containing a new hostname to set (optional)
136 -- @return              String containing the system hostname
137 function hostname(newname)
138         if type(newname) == "string" and #newname > 0 then
139                 fs.writefile( "/proc/sys/kernel/hostname", newname )
140                 return newname
141         else
142                 return nixio.uname().nodename
143         end
144 end
145
146 --- Returns the contents of a documented referred by an URL.
147 -- @param url    The URL to retrieve
148 -- @param stream Return a stream instead of a buffer
149 -- @param target Directly write to target file name
150 -- @return              String containing the contents of given the URL
151 function httpget(url, stream, target)
152         if not target then
153                 local source = stream and io.popen or luci.util.exec
154                 return source("wget -qO- '"..url:gsub("'", "").."'")
155         else
156                 return os.execute("wget -qO '%s' '%s'" %
157                         {target:gsub("'", ""), url:gsub("'", "")})
158         end
159 end
160
161 --- Returns the system load average values.
162 -- @return      String containing the average load value 1 minute ago
163 -- @return      String containing the average load value 5 minutes ago
164 -- @return      String containing the average load value 15 minutes ago
165 function loadavg()
166         local info = nixio.sysinfo()
167         return info.loads[1], info.loads[2], info.loads[3]
168 end
169
170 --- Initiate a system reboot.
171 -- @return      Return value of os.execute()
172 function reboot()
173         return os.execute("reboot >/dev/null 2>&1")
174 end
175
176 --- Returns the system type, cpu name and installed physical memory.
177 -- @return      String containing the system or platform identifier
178 -- @return      String containing hardware model information
179 -- @return      String containing the total memory amount in kB
180 -- @return      String containing the memory used for caching in kB
181 -- @return      String containing the memory used for buffering in kB
182 -- @return      String containing the free memory amount in kB
183 function sysinfo()
184         local cpuinfo = fs.readfile("/proc/cpuinfo")
185         local meminfo = fs.readfile("/proc/meminfo")
186
187         local system = cpuinfo:match("system typ.-:%s*([^\n]+)")
188         local model = ""
189         local memtotal = tonumber(meminfo:match("MemTotal:%s*(%d+)"))
190         local memcached = tonumber(meminfo:match("\nCached:%s*(%d+)"))
191         local memfree = tonumber(meminfo:match("MemFree:%s*(%d+)"))
192         local membuffers = tonumber(meminfo:match("Buffers:%s*(%d+)"))
193
194         if not system then
195                 system = nixio.uname().machine
196                 model = cpuinfo:match("model name.-:%s*([^\n]+)")
197                 if not model then
198                         model = cpuinfo:match("Processor.-:%s*([^\n]+)")
199                 end
200         else
201                 model = cpuinfo:match("cpu model.-:%s*([^\n]+)")
202         end
203
204         return system, model, memtotal, memcached, membuffers, memfree
205 end
206
207 --- Retrieves the output of the "logread" command.
208 -- @return      String containing the current log buffer
209 function syslog()
210         return luci.util.exec("logread")
211 end
212
213 --- Retrieves the output of the "dmesg" command.
214 -- @return      String containing the current log buffer
215 function dmesg()
216         return luci.util.exec("dmesg")
217 end
218
219 --- Generates a random id with specified length.
220 -- @param bytes Number of bytes for the unique id
221 -- @return              String containing hex encoded id
222 function uniqueid(bytes)
223         local rand = fs.readfile("/dev/urandom", bytes)
224         return rand and nixio.bin.hexlify(rand)
225 end
226
227 --- Returns the current system uptime stats.
228 -- @return      String containing total uptime in seconds
229 function uptime()
230         return nixio.sysinfo().uptime
231 end
232
233
234 --- LuCI system utilities / network related functions.
235 -- @class       module
236 -- @name        luci.sys.net
237 net = {}
238
239 --- Returns the current arp-table entries as two-dimensional table.
240 -- @return      Table of table containing the current arp entries.
241 --                      The following fields are defined for arp entry objects:
242 --                      { "IP address", "HW address", "HW type", "Flags", "Mask", "Device" }
243 function net.arptable(callback)
244         return _parse_delimited_table(io.lines("/proc/net/arp"), "%s%s+", callback)
245 end
246
247 --- Returns conntrack information
248 -- @return      Table with the currently tracked IP connections
249 function net.conntrack(callback)
250         local connt = {}
251         if fs.access("/proc/net/nf_conntrack", "r") then
252                 for line in io.lines("/proc/net/nf_conntrack") do
253                         line = line:match "^(.-( [^ =]+=).-)%2"
254                         local entry, flags = _parse_mixed_record(line, " +")
255                         entry.layer3 = flags[1]
256                         entry.layer4 = flags[3]
257                         for i=1, #entry do
258                                 entry[i] = nil
259                         end
260
261                         if callback then
262                                 callback(entry)
263                         else
264                                 connt[#connt+1] = entry
265                         end
266                 end
267         elseif fs.access("/proc/net/ip_conntrack", "r") then
268                 for line in io.lines("/proc/net/ip_conntrack") do
269                         line = line:match "^(.-( [^ =]+=).-)%2"
270                         local entry, flags = _parse_mixed_record(line, " +")
271                         entry.layer3 = "ipv4"
272                         entry.layer4 = flags[1]
273                         for i=1, #entry do
274                                 entry[i] = nil
275                         end
276
277                         if callback then
278                                 callback(entry)
279                         else
280                                 connt[#connt+1] = entry
281                         end
282                 end
283         else
284                 return nil
285         end
286         return connt
287 end
288
289 --- Determine the current IPv4 default route. If multiple default routes exist,
290 -- return the one with the lowest metric.
291 -- @return      Table with the properties of the current default route.
292 --                      The following fields are defined:
293 --                      { "dest", "gateway", "metric", "refcount", "usecount", "irtt",
294 --                        "flags", "device" }
295 function net.defaultroute()
296         local route
297
298         net.routes(function(rt)
299                 if rt.dest:prefix() == 0 and (not route or route.metric > rt.metric) then
300                         route = rt
301                 end
302         end)
303
304         return route
305 end
306
307 --- Determine the current IPv6 default route. If multiple default routes exist,
308 -- return the one with the lowest metric.
309 -- @return      Table with the properties of the current default route.
310 --                      The following fields are defined:
311 --                      { "source", "dest", "nexthop", "metric", "refcount", "usecount",
312 --                        "flags", "device" }
313 function net.defaultroute6()
314         local route
315
316         net.routes6(function(rt)
317                 if rt.dest:prefix() == 0 and (not route or route.metric > rt.metric) then
318                         route = rt
319                 end
320         end)
321
322         return route
323 end
324
325 --- Determine the names of available network interfaces.
326 -- @return      Table containing all current interface names
327 function net.devices()
328         local devs = {}
329         for k, v in ipairs(nixio.getifaddrs()) do
330                 if v.family == "packet" then
331                         devs[#devs+1] = v.name
332                 end
333         end
334         return devs
335 end
336
337
338 --- Return information about available network interfaces.
339 -- @return      Table containing all current interface names and their information
340 function net.deviceinfo()
341         local devs = {}
342         for k, v in ipairs(nixio.getifaddrs()) do
343                 if v.family == "packet" then
344                         local d = v.data
345                         d[1] = d.rx_bytes
346                         d[2] = d.rx_packets
347                         d[3] = d.rx_errors
348                         d[4] = d.rx_dropped
349                         d[5] = 0
350                         d[6] = 0
351                         d[7] = 0
352                         d[8] = d.multicast
353                         d[9] = d.tx_bytes
354                         d[10] = d.tx_packets
355                         d[11] = d.tx_errors
356                         d[12] = d.tx_dropped
357                         d[13] = 0
358                         d[14] = d.collisions
359                         d[15] = 0
360                         d[16] = 0
361                         devs[v.name] = d
362                 end
363         end
364         return devs
365 end
366
367
368 -- Determine the MAC address belonging to the given IP address.
369 -- @param ip    IPv4 address
370 -- @return              String containing the MAC address or nil if it cannot be found
371 function net.ip4mac(ip)
372         local mac = nil
373         net.arptable(function(e)
374                 if e["IP address"] == ip then
375                         mac = e["HW address"]
376                 end
377         end)
378         return mac
379 end
380
381 --- Returns the current kernel routing table entries.
382 -- @return      Table of tables with properties of the corresponding routes.
383 --                      The following fields are defined for route entry tables:
384 --                      { "dest", "gateway", "metric", "refcount", "usecount", "irtt",
385 --                        "flags", "device" }
386 function net.routes(callback)
387         local routes = { }
388
389         for line in io.lines("/proc/net/route") do
390
391                 local dev, dst_ip, gateway, flags, refcnt, usecnt, metric,
392                           dst_mask, mtu, win, irtt = line:match(
393                         "([^%s]+)\t([A-F0-9]+)\t([A-F0-9]+)\t([A-F0-9]+)\t" ..
394                         "(%d+)\t(%d+)\t(%d+)\t([A-F0-9]+)\t(%d+)\t(%d+)\t(%d+)"
395                 )
396
397                 if dev then
398                         gateway  = luci.ip.Hex( gateway,  32, luci.ip.FAMILY_INET4 )
399                         dst_mask = luci.ip.Hex( dst_mask, 32, luci.ip.FAMILY_INET4 )
400                         dst_ip   = luci.ip.Hex(
401                                 dst_ip, dst_mask:prefix(dst_mask), luci.ip.FAMILY_INET4
402                         )
403
404                         local rt = {
405                                 dest     = dst_ip,
406                                 gateway  = gateway,
407                                 metric   = tonumber(metric),
408                                 refcount = tonumber(refcnt),
409                                 usecount = tonumber(usecnt),
410                                 mtu      = tonumber(mtu),
411                                 window   = tonumber(window),
412                                 irtt     = tonumber(irtt),
413                                 flags    = tonumber(flags, 16),
414                                 device   = dev
415                         }
416
417                         if callback then
418                                 callback(rt)
419                         else
420                                 routes[#routes+1] = rt
421                         end
422                 end
423         end
424
425         return routes
426 end
427
428 --- Returns the current ipv6 kernel routing table entries.
429 -- @return      Table of tables with properties of the corresponding routes.
430 --                      The following fields are defined for route entry tables:
431 --                      { "source", "dest", "nexthop", "metric", "refcount", "usecount",
432 --                        "flags", "device" }
433 function net.routes6(callback)
434         if fs.access("/proc/net/ipv6_route", "r") then
435                 local routes = { }
436
437                 for line in io.lines("/proc/net/ipv6_route") do
438
439                         local dst_ip, dst_prefix, src_ip, src_prefix, nexthop,
440                                   metric, refcnt, usecnt, flags, dev = line:match(
441                                 "([a-f0-9]+) ([a-f0-9]+) " ..
442                                 "([a-f0-9]+) ([a-f0-9]+) " ..
443                                 "([a-f0-9]+) ([a-f0-9]+) " ..
444                                 "([a-f0-9]+) ([a-f0-9]+) " ..
445                                 "([a-f0-9]+) +([^%s]+)"
446                         )
447
448                         src_ip = luci.ip.Hex(
449                                 src_ip, tonumber(src_prefix, 16), luci.ip.FAMILY_INET6, false
450                         )
451
452                         dst_ip = luci.ip.Hex(
453                                 dst_ip, tonumber(dst_prefix, 16), luci.ip.FAMILY_INET6, false
454                         )
455
456                         nexthop = luci.ip.Hex( nexthop, 128, luci.ip.FAMILY_INET6, false )
457
458                         local rt = {
459                                 source   = src_ip,
460                                 dest     = dst_ip,
461                                 nexthop  = nexthop,
462                                 metric   = tonumber(metric, 16),
463                                 refcount = tonumber(refcnt, 16),
464                                 usecount = tonumber(usecnt, 16),
465                                 flags    = tonumber(flags, 16),
466                                 device   = dev
467                         }
468
469                         if callback then
470                                 callback(rt)
471                         else
472                                 routes[#routes+1] = rt
473                         end
474                 end
475
476                 return routes
477         end
478 end
479
480 --- Tests whether the given host responds to ping probes.
481 -- @param host  String containing a hostname or IPv4 address
482 -- @return              Number containing 0 on success and >= 1 on error
483 function net.pingtest(host)
484         return os.execute("ping -c1 '"..host:gsub("'", '').."' >/dev/null 2>&1")
485 end
486
487
488 --- LuCI system utilities / process related functions.
489 -- @class       module
490 -- @name        luci.sys.process
491 process = {}
492
493 --- Get the current process id.
494 -- @class function
495 -- @name  process.info
496 -- @return      Number containing the current pid
497 function process.info(key)
498         local s = {uid = nixio.getuid(), gid = nixio.getgid()}
499         return not key and s or s[key]
500 end
501
502 --- Retrieve information about currently running processes.
503 -- @return      Table containing process information
504 function process.list()
505         local data = {}
506         local k
507         local ps = luci.util.execi("top -bn1")
508
509         if not ps then
510                 return
511         end
512
513         while true do
514                 local line = ps()
515                 if not line then
516                         return
517                 end
518
519                 k = luci.util.split(luci.util.trim(line), "%s+", nil, true)
520                 if k[1] == "PID" then
521                         break
522                 end
523         end
524
525         for line in ps do
526                 local row = {}
527
528                 line = luci.util.trim(line)
529                 for i, value in ipairs(luci.util.split(line, "%s+", #k-1, true)) do
530                         row[k[i]] = value
531                 end
532
533                 local pid = tonumber(row[k[1]])
534                 if pid then
535                         data[pid] = row
536                 end
537         end
538
539         return data
540 end
541
542 --- Set the gid of a process identified by given pid.
543 -- @param gid   Number containing the Unix group id
544 -- @return              Boolean indicating successful operation
545 -- @return              String containing the error message if failed
546 -- @return              Number containing the error code if failed
547 function process.setgroup(gid)
548         return nixio.setgid(gid)
549 end
550
551 --- Set the uid of a process identified by given pid.
552 -- @param uid   Number containing the Unix user id
553 -- @return              Boolean indicating successful operation
554 -- @return              String containing the error message if failed
555 -- @return              Number containing the error code if failed
556 function process.setuser(uid)
557         return nixio.setuid(uid)
558 end
559
560 --- Send a signal to a process identified by given pid.
561 -- @class function
562 -- @name  process.signal
563 -- @param pid   Number containing the process id
564 -- @param sig   Signal to send (default: 15 [SIGTERM])
565 -- @return              Boolean indicating successful operation
566 -- @return              Number containing the error code if failed
567 process.signal = nixio.kill
568
569
570 --- LuCI system utilities / user related functions.
571 -- @class       module
572 -- @name        luci.sys.user
573 user = {}
574
575 --- Retrieve user informations for given uid.
576 -- @class               function
577 -- @name                getuser
578 -- @param uid   Number containing the Unix user id
579 -- @return              Table containing the following fields:
580 --                              { "uid", "gid", "name", "passwd", "dir", "shell", "gecos" }
581 user.getuser = nixio.getpw
582
583 --- Test whether given string matches the password of a given system user.
584 -- @param username      String containing the Unix user name
585 -- @param pass          String containing the password to compare
586 -- @return                      Boolean indicating wheather the passwords are equal
587 function user.checkpasswd(username, pass)
588         local pwe = nixio.getsp and nixio.getsp(username) or nixio.getpw(username)
589         local pwh = pwe and (pwe.pwdp or pwe.passwd)
590         if not pwh or #pwh < 1 or pwh ~= "!" and nixio.crypt(pass, pwh) ~= pwh then
591                 return false
592         else
593                 return true
594         end
595 end
596
597 --- Change the password of given user.
598 -- @param username      String containing the Unix user name
599 -- @param password      String containing the password to compare
600 -- @return                      Number containing 0 on success and >= 1 on error
601 function user.setpasswd(username, password)
602         if password then
603                 password = password:gsub("'", "")
604         end
605
606         if username then
607                 username = username:gsub("'", "")
608         end
609
610         local cmd = "(echo '"..password.."';sleep 1;echo '"..password.."')|"
611         cmd = cmd .. "passwd '"..username.."' >/dev/null 2>&1"
612         return os.execute(cmd)
613 end
614
615
616 --- LuCI system utilities / wifi related functions.
617 -- @class       module
618 -- @name        luci.sys.wifi
619 wifi = {}
620
621 --- Get wireless information for given interface.
622 -- @param ifname        String containing the interface name
623 -- @return              A wrapped iwinfo object instance
624 function wifi.getiwinfo(ifname)
625         local t = iwinfo.type(ifname)
626         if t then
627                 local x = iwinfo[t]
628                 return setmetatable({}, {
629                         __index = function(t, k)
630                                 if x[k] then return x[k](ifname) end
631                         end
632                 })
633         end
634 end
635
636 --- Get iwconfig output for all wireless devices.
637 -- @return      Table of tables containing the iwconfing output for each wifi device
638 function wifi.getiwconfig()
639         local cnt = luci.util.exec("PATH=/sbin:/usr/sbin iwconfig 2>/dev/null")
640         local iwc = {}
641
642         for i, l in pairs(luci.util.split(luci.util.trim(cnt), "\n\n")) do
643                 local k = l:match("^(.-) ")
644                 l = l:gsub("^(.-) +", "", 1)
645                 if k then
646                         local entry, flags = _parse_mixed_record(l)
647                         if entry then
648                                 entry.flags = flags
649                         end
650                         iwc[k] = entry
651                 end
652         end
653
654         return iwc
655 end
656
657 --- Get iwlist scan output from all wireless devices.
658 -- @return      Table of tables contaiing all scan results
659 function wifi.iwscan(iface)
660         local siface = iface or ""
661         local cnt = luci.util.exec("iwlist "..siface.." scan 2>/dev/null")
662         local iws = {}
663
664         for i, l in pairs(luci.util.split(luci.util.trim(cnt), "\n\n")) do
665                 local k = l:match("^(.-) ")
666                 l = l:gsub("^[^\n]+", "", 1)
667                 l = luci.util.trim(l)
668                 if k then
669                         iws[k] = {}
670                         for j, c in pairs(luci.util.split(l, "\n          Cell")) do
671                                 c = c:gsub("^(.-)- ", "", 1)
672                                 c = luci.util.split(c, "\n", 7)
673                                 c = table.concat(c, "\n", 1)
674                                 local entry, flags = _parse_mixed_record(c)
675                                 if entry then
676                                         entry.flags = flags
677                                 end
678                                 table.insert(iws[k], entry)
679                         end
680                 end
681         end
682
683         return iface and (iws[iface] or {}) or iws
684 end
685
686 --- Get available channels from given wireless iface.
687 -- @param iface Wireless interface (optional)
688 -- @return              Table of available channels
689 function wifi.channels(iface)
690         local cmd = "iwlist " .. ( iface or "" ) .. " freq 2>/dev/null"
691         local cns = { }
692
693         local fd = io.popen(cmd)
694         if fd then
695                 local ln, c, f
696                 while true do
697                         ln = fd:read("*l")
698                         if not ln then break end
699                         c, f = ln:match("Channel (%d+) : (%d+%.%d+) GHz")
700                         if c and f then
701                                 cns[tonumber(c)] = tonumber(f)
702                         end
703                 end
704                 fd:close()
705         end
706
707         if not next(cns) then
708                 cns = {
709                         2.412, 2.417, 2.422, 2.427, 2.432, 2.437,
710                         2.442, 2.447, 2.452, 2.457, 2.462
711                 }
712         end
713
714         return cns
715 end
716
717
718 --- LuCI system utilities / init related functions.
719 -- @class       module
720 -- @name        luci.sys.init
721 init = {}
722 init.dir = "/etc/init.d/"
723
724 --- Get the names of all installed init scripts
725 -- @return      Table containing the names of all inistalled init scripts
726 function init.names()
727         local names = { }
728         for name in fs.glob(init.dir.."*") do
729                 names[#names+1] = fs.basename(name)
730         end
731         return names
732 end
733
734 --- Test whether the given init script is enabled
735 -- @param name  Name of the init script
736 -- @return              Boolean indicating whether init is enabled
737 function init.enabled(name)
738         if fs.access(init.dir..name) then
739                 return ( call(init.dir..name.." enabled") == 0 )
740         end
741         return false
742 end
743
744 --- Get the index of he given init script
745 -- @param name  Name of the init script
746 -- @return              Numeric index value
747 function init.index(name)
748         if fs.access(init.dir..name) then
749                 return call("source "..init.dir..name.."; exit $START")
750         end
751 end
752
753 --- Enable the given init script
754 -- @param name  Name of the init script
755 -- @return              Boolean indicating success
756 function init.enable(name)
757         if fs.access(init.dir..name) then
758                 return ( call(init.dir..name.." enable") == 1 )
759         end
760 end
761
762 --- Disable the given init script
763 -- @param name  Name of the init script
764 -- @return              Boolean indicating success
765 function init.disable(name)
766         if fs.access(init.dir..name) then
767                 return ( call(init.dir..name.." disable") == 0 )
768         end
769 end
770
771
772 -- Internal functions
773
774 function _parse_delimited_table(iter, delimiter, callback)
775         delimiter = delimiter or "%s+"
776
777         local data  = {}
778         local trim  = luci.util.trim
779         local split = luci.util.split
780
781         local keys = split(trim(iter()), delimiter, nil, true)
782         for i, j in pairs(keys) do
783                 keys[i] = trim(keys[i])
784         end
785
786         for line in iter do
787                 local row = {}
788                 line = trim(line)
789                 if #line > 0 then
790                         for i, j in pairs(split(line, delimiter, nil, true)) do
791                                 if keys[i] then
792                                         row[keys[i]] = j
793                                 end
794                         end
795                 end
796
797                 if callback then
798                         callback(row)
799                 else
800                         data[#data+1] = row
801                 end
802         end
803
804         return data
805 end
806
807 function _parse_mixed_record(cnt, delimiter)
808         delimiter = delimiter or "  "
809         local data = {}
810         local flags = {}
811
812         for i, l in pairs(luci.util.split(luci.util.trim(cnt), "\n")) do
813                 for j, f in pairs(luci.util.split(luci.util.trim(l), delimiter, nil, true)) do
814                         local k, x, v = f:match('([^%s][^:=]*) *([:=]*) *"*([^\n"]*)"*')
815
816                         if k then
817                                 if x == "" then
818                                         table.insert(flags, k)
819                                 else
820                                         data[k] = v
821                                 end
822                         end
823                 end
824         end
825
826         return data, flags
827 end