d57077026f0ae9c41f44eb14f6c5146e02f9a95b
[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
34 local luci  = {}
35 luci.util   = require "luci.util"
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                 fs.writefile( "/proc/sys/kernel/hostname", newname )
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 = fs.readfile("/proc/cpuinfo")
184         local meminfo = 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 = 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 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 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         net.arptable(function(e)
373                 if e["IP address"] == ip then
374                         mac = e["HW address"]
375                 end
376         end)
377         return mac
378 end
379
380 --- Returns the current kernel routing table entries.
381 -- @return      Table of tables with properties of the corresponding routes.
382 --                      The following fields are defined for route entry tables:
383 --                      { "dest", "gateway", "metric", "refcount", "usecount", "irtt",
384 --                        "flags", "device" }
385 function net.routes(callback)
386         local routes = { }
387
388         for line in io.lines("/proc/net/route") do
389
390                 local dev, dst_ip, gateway, flags, refcnt, usecnt, metric,
391                           dst_mask, mtu, win, irtt = line:match(
392                         "([^%s]+)\t([A-F0-9]+)\t([A-F0-9]+)\t([A-F0-9]+)\t" ..
393                         "(%d+)\t(%d+)\t(%d+)\t([A-F0-9]+)\t(%d+)\t(%d+)\t(%d+)"
394                 )
395
396                 if dev then
397                         gateway  = luci.ip.Hex( gateway,  32, luci.ip.FAMILY_INET4 )
398                         dst_mask = luci.ip.Hex( dst_mask, 32, luci.ip.FAMILY_INET4 )
399                         dst_ip   = luci.ip.Hex(
400                                 dst_ip, dst_mask:prefix(dst_mask), luci.ip.FAMILY_INET4
401                         )
402
403                         local rt = {
404                                 dest     = dst_ip,
405                                 gateway  = gateway,
406                                 metric   = tonumber(metric),
407                                 refcount = tonumber(refcnt),
408                                 usecount = tonumber(usecnt),
409                                 mtu      = tonumber(mtu),
410                                 window   = tonumber(window),
411                                 irtt     = tonumber(irtt),
412                                 flags    = tonumber(flags, 16),
413                                 device   = dev
414                         }
415
416                         if callback then
417                                 callback(rt)
418                         else
419                                 routes[#routes+1] = rt
420                         end
421                 end
422         end
423
424         return routes
425 end
426
427 --- Returns the current ipv6 kernel routing table entries.
428 -- @return      Table of tables with properties of the corresponding routes.
429 --                      The following fields are defined for route entry tables:
430 --                      { "source", "dest", "nexthop", "metric", "refcount", "usecount",
431 --                        "flags", "device" }
432 function net.routes6(callback)
433         if fs.access("/proc/net/ipv6_route", "r") then
434                 local routes = { }
435
436                 for line in io.lines("/proc/net/ipv6_route") do
437
438                         local dst_ip, dst_prefix, src_ip, src_prefix, nexthop,
439                                   metric, refcnt, usecnt, flags, dev = line:match(
440                                 "([a-f0-9]+) ([a-f0-9]+) " ..
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]+) +([^%s]+)"
445                         )
446
447                         src_ip = luci.ip.Hex(
448                                 src_ip, tonumber(src_prefix, 16), luci.ip.FAMILY_INET6, false
449                         )
450
451                         dst_ip = luci.ip.Hex(
452                                 dst_ip, tonumber(dst_prefix, 16), luci.ip.FAMILY_INET6, false
453                         )
454
455                         nexthop = luci.ip.Hex( nexthop, 128, luci.ip.FAMILY_INET6, false )
456
457                         local rt = {
458                                 source   = src_ip,
459                                 dest     = dst_ip,
460                                 nexthop  = nexthop,
461                                 metric   = tonumber(metric, 16),
462                                 refcount = tonumber(refcnt, 16),
463                                 usecount = tonumber(usecnt, 16),
464                                 flags    = tonumber(flags, 16),
465                                 device   = dev
466                         }
467
468                         if callback then
469                                 callback(rt)
470                         else
471                                 routes[#routes+1] = rt
472                         end
473                 end
474
475                 return routes
476         end
477 end
478
479 --- Tests whether the given host responds to ping probes.
480 -- @param host  String containing a hostname or IPv4 address
481 -- @return              Number containing 0 on success and >= 1 on error
482 function net.pingtest(host)
483         return os.execute("ping -c1 '"..host:gsub("'", '').."' >/dev/null 2>&1")
484 end
485
486
487 --- LuCI system utilities / process related functions.
488 -- @class       module
489 -- @name        luci.sys.process
490 process = {}
491
492 --- Get the current process id.
493 -- @class function
494 -- @name  process.info
495 -- @return      Number containing the current pid
496 function process.info(key)
497         local s = {uid = nixio.getuid(), gid = nixio.getgid()}
498         return not key and s or s[key]
499 end
500
501 --- Retrieve information about currently running processes.
502 -- @return      Table containing process information
503 function process.list()
504         local data = {}
505         local k
506         local ps = luci.util.execi("top -bn1")
507
508         if not ps then
509                 return
510         end
511
512         while true do
513                 local line = ps()
514                 if not line then
515                         return
516                 end
517
518                 k = luci.util.split(luci.util.trim(line), "%s+", nil, true)
519                 if k[1] == "PID" then
520                         break
521                 end
522         end
523
524         for line in ps do
525                 local row = {}
526
527                 line = luci.util.trim(line)
528                 for i, value in ipairs(luci.util.split(line, "%s+", #k-1, true)) do
529                         row[k[i]] = value
530                 end
531
532                 local pid = tonumber(row[k[1]])
533                 if pid then
534                         data[pid] = row
535                 end
536         end
537
538         return data
539 end
540
541 --- Set the gid of a process identified by given pid.
542 -- @param gid   Number containing the Unix group id
543 -- @return              Boolean indicating successful operation
544 -- @return              String containing the error message if failed
545 -- @return              Number containing the error code if failed
546 function process.setgroup(gid)
547         return nixio.setgid(gid)
548 end
549
550 --- Set the uid of a process identified by given pid.
551 -- @param uid   Number containing the Unix user id
552 -- @return              Boolean indicating successful operation
553 -- @return              String containing the error message if failed
554 -- @return              Number containing the error code if failed
555 function process.setuser(uid)
556         return nixio.setuid(uid)
557 end
558
559 --- Send a signal to a process identified by given pid.
560 -- @class function
561 -- @name  process.signal
562 -- @param pid   Number containing the process id
563 -- @param sig   Signal to send (default: 15 [SIGTERM])
564 -- @return              Boolean indicating successful operation
565 -- @return              Number containing the error code if failed
566 process.signal = nixio.kill
567
568
569 --- LuCI system utilities / user related functions.
570 -- @class       module
571 -- @name        luci.sys.user
572 user = {}
573
574 --- Retrieve user informations for given uid.
575 -- @class               function
576 -- @name                getuser
577 -- @param uid   Number containing the Unix user id
578 -- @return              Table containing the following fields:
579 --                              { "uid", "gid", "name", "passwd", "dir", "shell", "gecos" }
580 user.getuser = nixio.getpw
581
582 --- Test whether given string matches the password of a given system user.
583 -- @param username      String containing the Unix user name
584 -- @param pass          String containing the password to compare
585 -- @return                      Boolean indicating wheather the passwords are equal
586 function user.checkpasswd(username, pass)
587         local pwe = nixio.getsp and nixio.getsp(username) or nixio.getpw(username)
588         local pwh = pwe and (pwe.pwdp or pwe.passwd)
589         if not pwh or #pwh < 1 or pwh ~= "!" and nixio.crypt(pass, pwh) ~= pwh then
590                 return false
591         else
592                 return true
593         end
594 end
595
596 --- Change the password of given user.
597 -- @param username      String containing the Unix user name
598 -- @param password      String containing the password to compare
599 -- @return                      Number containing 0 on success and >= 1 on error
600 function user.setpasswd(username, password)
601         if password then
602                 password = password:gsub("'", "")
603         end
604
605         if username then
606                 username = username:gsub("'", "")
607         end
608
609         local cmd = "(echo '"..password.."';sleep 1;echo '"..password.."')|"
610         cmd = cmd .. "passwd '"..username.."' >/dev/null 2>&1"
611         return os.execute(cmd)
612 end
613
614
615 --- LuCI system utilities / wifi related functions.
616 -- @class       module
617 -- @name        luci.sys.wifi
618 wifi = {}
619
620 --- Get iwconfig output for all wireless devices.
621 -- @return      Table of tables containing the iwconfing output for each wifi device
622 function wifi.getiwconfig()
623         local cnt = luci.util.exec("PATH=/sbin:/usr/sbin iwconfig 2>/dev/null")
624         local iwc = {}
625
626         for i, l in pairs(luci.util.split(luci.util.trim(cnt), "\n\n")) do
627                 local k = l:match("^(.-) ")
628                 l = l:gsub("^(.-) +", "", 1)
629                 if k then
630                         local entry, flags = _parse_mixed_record(l)
631                         if entry then
632                                 entry.flags = flags
633                         end
634                         iwc[k] = entry
635                 end
636         end
637
638         return iwc
639 end
640
641 --- Get iwlist scan output from all wireless devices.
642 -- @return      Table of tables contaiing all scan results
643 function wifi.iwscan(iface)
644         local siface = iface or ""
645         local cnt = luci.util.exec("iwlist "..siface.." scan 2>/dev/null")
646         local iws = {}
647
648         for i, l in pairs(luci.util.split(luci.util.trim(cnt), "\n\n")) do
649                 local k = l:match("^(.-) ")
650                 l = l:gsub("^[^\n]+", "", 1)
651                 l = luci.util.trim(l)
652                 if k then
653                         iws[k] = {}
654                         for j, c in pairs(luci.util.split(l, "\n          Cell")) do
655                                 c = c:gsub("^(.-)- ", "", 1)
656                                 c = luci.util.split(c, "\n", 7)
657                                 c = table.concat(c, "\n", 1)
658                                 local entry, flags = _parse_mixed_record(c)
659                                 if entry then
660                                         entry.flags = flags
661                                 end
662                                 table.insert(iws[k], entry)
663                         end
664                 end
665         end
666
667         return iface and (iws[iface] or {}) or iws
668 end
669
670 --- Get available channels from given wireless iface.
671 -- @param iface Wireless interface (optional)
672 -- @return              Table of available channels
673 function wifi.channels(iface)
674         local cmd = "iwlist " .. ( iface or "" ) .. " freq 2>/dev/null"
675         local cns = { }
676
677         local fd = io.popen(cmd)
678         if fd then
679                 local ln, c, f
680                 while true do
681                         ln = fd:read("*l")
682                         if not ln then break end
683                         c, f = ln:match("Channel (%d+) : (%d+%.%d+) GHz")
684                         if c and f then
685                                 cns[tonumber(c)] = tonumber(f)
686                         end
687                 end
688                 fd:close()
689         end
690
691         if not next(cns) then
692                 cns = {
693                         2.412, 2.417, 2.422, 2.427, 2.432, 2.437,
694                         2.442, 2.447, 2.452, 2.457, 2.462
695                 }
696         end
697
698         return cns
699 end
700
701
702 --- LuCI system utilities / init related functions.
703 -- @class       module
704 -- @name        luci.sys.init
705 init = {}
706 init.dir = "/etc/init.d/"
707
708 --- Get the names of all installed init scripts
709 -- @return      Table containing the names of all inistalled init scripts
710 function init.names()
711         local names = { }
712         for name in fs.glob(init.dir.."*") do
713                 names[#names+1] = fs.basename(name)
714         end
715         return names
716 end
717
718 --- Test whether the given init script is enabled
719 -- @param name  Name of the init script
720 -- @return              Boolean indicating whether init is enabled
721 function init.enabled(name)
722         if fs.access(init.dir..name) then
723                 return ( call(init.dir..name.." enabled") == 0 )
724         end
725         return false
726 end
727
728 --- Get the index of he given init script
729 -- @param name  Name of the init script
730 -- @return              Numeric index value
731 function init.index(name)
732         if fs.access(init.dir..name) then
733                 return call("source "..init.dir..name.."; exit $START")
734         end
735 end
736
737 --- Enable the given init script
738 -- @param name  Name of the init script
739 -- @return              Boolean indicating success
740 function init.enable(name)
741         if fs.access(init.dir..name) then
742                 return ( call(init.dir..name.." enable") == 1 )
743         end
744 end
745
746 --- Disable the given init script
747 -- @param name  Name of the init script
748 -- @return              Boolean indicating success
749 function init.disable(name)
750         if fs.access(init.dir..name) then
751                 return ( call(init.dir..name.." disable") == 0 )
752         end
753 end
754
755
756 -- Internal functions
757
758 function _parse_delimited_table(iter, delimiter, callback)
759         delimiter = delimiter or "%s+"
760
761         local data  = {}
762         local trim  = luci.util.trim
763         local split = luci.util.split
764
765         local keys = split(trim(iter()), delimiter, nil, true)
766         for i, j in pairs(keys) do
767                 keys[i] = trim(keys[i])
768         end
769
770         for line in iter do
771                 local row = {}
772                 line = trim(line)
773                 if #line > 0 then
774                         for i, j in pairs(split(line, delimiter, nil, true)) do
775                                 if keys[i] then
776                                         row[keys[i]] = j
777                                 end
778                         end
779                 end
780
781                 if callback then
782                         callback(row)
783                 else
784                         data[#data+1] = row
785                 end
786         end
787
788         return data
789 end
790
791 function _parse_mixed_record(cnt, delimiter)
792         delimiter = delimiter or "  "
793         local data = {}
794         local flags = {}
795
796         for i, l in pairs(luci.util.split(luci.util.trim(cnt), "\n")) do
797                 for j, f in pairs(luci.util.split(luci.util.trim(l), delimiter, nil, true)) do
798                         local k, x, v = f:match('([^%s][^:=]*) *([:=]*) *"*([^\n"]*)"*')
799
800                         if k then
801                                 if x == "" then
802                                         table.insert(flags, k)
803                                 else
804                                         data[k] = v
805                                 end
806                         end
807                 end
808         end
809
810         return data, flags
811 end