Minor fixes
[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 t = iwinfo.type(iface or "")
691         local cns
692         if t and iwinfo[t] then
693                 cns = iwinfo[t].freqlist(iface)
694         end
695
696         if not cns or #cns == 0 then
697                 cns = {
698                         {channel =  1, mhz = 2412},
699                         {channel =  2, mhz = 2417},
700                         {channel =  3, mhz = 2422},
701                         {channel =  4, mhz = 2427},
702                         {channel =  5, mhz = 2432},
703                         {channel =  6, mhz = 2437},
704                         {channel =  7, mhz = 2442},
705                         {channel =  8, mhz = 2447},
706                         {channel =  9, mhz = 2452},
707                         {channel = 10, mhz = 2457},
708                         {channel = 11, mhz = 2462}
709                 }
710         end
711
712         return cns
713 end
714
715
716 --- LuCI system utilities / init related functions.
717 -- @class       module
718 -- @name        luci.sys.init
719 init = {}
720 init.dir = "/etc/init.d/"
721
722 --- Get the names of all installed init scripts
723 -- @return      Table containing the names of all inistalled init scripts
724 function init.names()
725         local names = { }
726         for name in fs.glob(init.dir.."*") do
727                 names[#names+1] = fs.basename(name)
728         end
729         return names
730 end
731
732 --- Test whether the given init script is enabled
733 -- @param name  Name of the init script
734 -- @return              Boolean indicating whether init is enabled
735 function init.enabled(name)
736         if fs.access(init.dir..name) then
737                 return ( call(init.dir..name.." enabled") == 0 )
738         end
739         return false
740 end
741
742 --- Get the index of he given init script
743 -- @param name  Name of the init script
744 -- @return              Numeric index value
745 function init.index(name)
746         if fs.access(init.dir..name) then
747                 return call("source "..init.dir..name.."; exit $START")
748         end
749 end
750
751 --- Enable the given init script
752 -- @param name  Name of the init script
753 -- @return              Boolean indicating success
754 function init.enable(name)
755         if fs.access(init.dir..name) then
756                 return ( call(init.dir..name.." enable") == 1 )
757         end
758 end
759
760 --- Disable the given init script
761 -- @param name  Name of the init script
762 -- @return              Boolean indicating success
763 function init.disable(name)
764         if fs.access(init.dir..name) then
765                 return ( call(init.dir..name.." disable") == 0 )
766         end
767 end
768
769
770 -- Internal functions
771
772 function _parse_delimited_table(iter, delimiter, callback)
773         delimiter = delimiter or "%s+"
774
775         local data  = {}
776         local trim  = luci.util.trim
777         local split = luci.util.split
778
779         local keys = split(trim(iter()), delimiter, nil, true)
780         for i, j in pairs(keys) do
781                 keys[i] = trim(keys[i])
782         end
783
784         for line in iter do
785                 local row = {}
786                 line = trim(line)
787                 if #line > 0 then
788                         for i, j in pairs(split(line, delimiter, nil, true)) do
789                                 if keys[i] then
790                                         row[keys[i]] = j
791                                 end
792                         end
793                 end
794
795                 if callback then
796                         callback(row)
797                 else
798                         data[#data+1] = row
799                 end
800         end
801
802         return data
803 end
804
805 function _parse_mixed_record(cnt, delimiter)
806         delimiter = delimiter or "  "
807         local data = {}
808         local flags = {}
809
810         for i, l in pairs(luci.util.split(luci.util.trim(cnt), "\n")) do
811                 for j, f in pairs(luci.util.split(luci.util.trim(l), delimiter, nil, true)) do
812                         local k, x, v = f:match('([^%s][^:=]*) *([:=]*) *"*([^\n"]*)"*')
813
814                         if k then
815                                 if x == "" then
816                                         table.insert(flags, k)
817                                 else
818                                         data[k] = v
819                                 end
820                         end
821                 end
822         end
823
824         return data, flags
825 end