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