libs/sys: split zoneinfo in tzdata and tzoffset and autoload on demand, saves yet...
[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 =
39         tonumber, ipairs, pairs, pcall, type
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()
243         return _parse_delimited_table(io.lines("/proc/net/arp"), "%s%s+")
244 end
245
246 --- Returns conntrack information
247 -- @return      Table with the currently tracked IP connections
248 function net.conntrack()
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                         connt[#connt+1] = entry
261                 end
262         elseif luci.fs.access("/proc/net/ip_conntrack", "r") then
263                 for line in io.lines("/proc/net/ip_conntrack") do
264                         line = line:match "^(.-( [^ =]+=).-)%2"
265                         local entry, flags = _parse_mixed_record(line, " +")
266                         entry.layer3 = "ipv4"
267                         entry.layer4 = flags[1]
268                         for i=1, #entry do
269                                 entry[i] = nil
270                         end
271
272                         connt[#connt+1] = entry
273                 end
274         else
275                 return nil
276         end
277         return connt
278 end
279
280 --- Determine the current IPv4 default route. If multiple default routes exist,
281 -- return the one with the lowest metric.
282 -- @return      Table with the properties of the current default route.
283 --                      The following fields are defined:
284 --                      { "dest", "gateway", "metric", "refcount", "usecount", "irtt",
285 --                        "flags", "device" }
286 function net.defaultroute()
287         local route
288
289         net.routes(function(rt)
290                 if rt.dest:prefix() == 0 and (not route or route.metric > rt.metric) then
291                         route = rt
292                 end
293         end)
294
295         return route
296 end
297
298 --- Determine the current IPv6 default route. If multiple default routes exist,
299 -- return the one with the lowest metric.
300 -- @return      Table with the properties of the current default route.
301 --                      The following fields are defined:
302 --                      { "source", "dest", "nexthop", "metric", "refcount", "usecount",
303 --                        "flags", "device" }
304 function net.defaultroute6()
305         local route
306
307         net.routes6(function(rt)
308                 if rt.dest:prefix() == 0 and (not route or route.metric > rt.metric) then
309                         route = rt
310                 end
311         end)
312
313         return route
314 end
315
316 --- Determine the names of available network interfaces.
317 -- @return      Table containing all current interface names
318 function net.devices()
319         local devs = {}
320         for k, v in ipairs(nixio.getifaddrs()) do
321                 if v.family == "packet" then
322                         devs[#devs+1] = v.name
323                 end
324         end
325         return devs
326 end
327
328
329 --- Return information about available network interfaces.
330 -- @return      Table containing all current interface names and their information
331 function net.deviceinfo()
332         local devs = {}
333         for k, v in ipairs(nixio.getifaddrs()) do
334                 if v.family == "packet" then
335                         local d = v.data
336                         d[1] = d.rx_bytes
337                         d[2] = d.rx_packets
338                         d[3] = d.rx_errors
339                         d[4] = d.rx_dropped
340                         d[5] = 0
341                         d[6] = 0
342                         d[7] = 0
343                         d[8] = d.multicast
344                         d[9] = d.tx_bytes
345                         d[10] = d.tx_packets
346                         d[11] = d.tx_errors
347                         d[12] = d.tx_dropped
348                         d[13] = 0
349                         d[14] = d.collisions
350                         d[15] = 0
351                         d[16] = 0
352                         devs[v.name] = d
353                 end
354         end
355         return devs
356 end
357
358
359 -- Determine the MAC address belonging to the given IP address.
360 -- @param ip    IPv4 address
361 -- @return              String containing the MAC address or nil if it cannot be found
362 function net.ip4mac(ip)
363         local mac = nil
364
365         for i, l in ipairs(net.arptable()) do
366                 if l["IP address"] == ip then
367                         mac = l["HW address"]
368                 end
369         end
370
371         return mac
372 end
373
374 --- Returns the current kernel routing table entries.
375 -- @return      Table of tables with properties of the corresponding routes.
376 --                      The following fields are defined for route entry tables:
377 --                      { "dest", "gateway", "metric", "refcount", "usecount", "irtt",
378 --                        "flags", "device" }
379 function net.routes(callback)
380         local routes = { }
381
382         for line in io.lines("/proc/net/route") do
383
384                 local dev, dst_ip, gateway, flags, refcnt, usecnt, metric,
385                           dst_mask, mtu, win, irtt = line:match(
386                         "([^%s]+)\t([A-F0-9]+)\t([A-F0-9]+)\t([A-F0-9]+)\t" ..
387                         "(%d+)\t(%d+)\t(%d+)\t([A-F0-9]+)\t(%d+)\t(%d+)\t(%d+)"
388                 )
389
390                 if dev then
391                         gateway  = luci.ip.Hex( gateway,  32, luci.ip.FAMILY_INET4 )
392                         dst_mask = luci.ip.Hex( dst_mask, 32, luci.ip.FAMILY_INET4 )
393                         dst_ip   = luci.ip.Hex(
394                                 dst_ip, dst_mask:prefix(dst_mask), luci.ip.FAMILY_INET4
395                         )
396
397                         local rt = {
398                                 dest     = dst_ip,
399                                 gateway  = gateway,
400                                 metric   = tonumber(metric),
401                                 refcount = tonumber(refcnt),
402                                 usecount = tonumber(usecnt),
403                                 mtu      = tonumber(mtu),
404                                 window   = tonumber(window),
405                                 irtt     = tonumber(irtt),
406                                 flags    = tonumber(flags, 16),
407                                 device   = dev
408                         }
409
410                         if callback then
411                                 callback(rt)
412                         else
413                                 routes[#routes+1] = rt
414                         end
415                 end
416         end
417
418         return routes
419 end
420
421 --- Returns the current ipv6 kernel routing table entries.
422 -- @return      Table of tables with properties of the corresponding routes.
423 --                      The following fields are defined for route entry tables:
424 --                      { "source", "dest", "nexthop", "metric", "refcount", "usecount",
425 --                        "flags", "device" }
426 function net.routes6(callback)
427         if luci.fs.access("/proc/net/ipv6_route", "r") then
428                 local routes = { }
429
430                 for line in io.lines("/proc/net/ipv6_route") do
431
432                         local dst_ip, dst_prefix, src_ip, src_prefix, nexthop,
433                                   metric, refcnt, usecnt, flags, dev = line:match(
434                                 "([a-f0-9]+) ([a-f0-9]+) " ..
435                                 "([a-f0-9]+) ([a-f0-9]+) " ..
436                                 "([a-f0-9]+) ([a-f0-9]+) " ..
437                                 "([a-f0-9]+) ([a-f0-9]+) " ..
438                                 "([a-f0-9]+) +([^%s]+)"
439                         )
440
441                         src_ip = luci.ip.Hex(
442                                 src_ip, tonumber(src_prefix, 16), luci.ip.FAMILY_INET6, false
443                         )
444
445                         dst_ip = luci.ip.Hex(
446                                 dst_ip, tonumber(dst_prefix, 16), luci.ip.FAMILY_INET6, false
447                         )
448
449                         nexthop = luci.ip.Hex( nexthop, 128, luci.ip.FAMILY_INET6, false )
450
451                         local rt = {
452                                 source   = src_ip,
453                                 dest     = dst_ip,
454                                 nexthop  = nexthop,
455                                 metric   = tonumber(metric, 16),
456                                 refcount = tonumber(refcnt, 16),
457                                 usecount = tonumber(usecnt, 16),
458                                 flags    = tonumber(flags, 16),
459                                 device   = dev
460                         }
461
462                         if callback then
463                                 callback(rt)
464                         else
465                                 routes[#routes+1] = rt
466                         end
467                 end
468
469                 return routes
470         end
471 end
472
473 --- Tests whether the given host responds to ping probes.
474 -- @param host  String containing a hostname or IPv4 address
475 -- @return              Number containing 0 on success and >= 1 on error
476 function net.pingtest(host)
477         return os.execute("ping -c1 '"..host:gsub("'", '').."' >/dev/null 2>&1")
478 end
479
480
481 --- LuCI system utilities / process related functions.
482 -- @class       module
483 -- @name        luci.sys.process
484 process = {}
485
486 --- Get the current process id.
487 -- @class function
488 -- @name  process.info
489 -- @return      Number containing the current pid
490 function process.info(key)
491         local s = {uid = nixio.getuid(), gid = nixio.getgid()}
492         return not key and s or s[key]
493 end
494
495 --- Retrieve information about currently running processes.
496 -- @return      Table containing process information
497 function process.list()
498         local data = {}
499         local k
500         local ps = luci.util.execi("top -bn1")
501
502         if not ps then
503                 return
504         end
505
506         while true do
507                 local line = ps()
508                 if not line then
509                         return
510                 end
511
512                 k = luci.util.split(luci.util.trim(line), "%s+", nil, true)
513                 if k[1] == "PID" then
514                         break
515                 end
516         end
517
518         for line in ps do
519                 local row = {}
520
521                 line = luci.util.trim(line)
522                 for i, value in ipairs(luci.util.split(line, "%s+", #k-1, true)) do
523                         row[k[i]] = value
524                 end
525
526                 local pid = tonumber(row[k[1]])
527                 if pid then
528                         data[pid] = row
529                 end
530         end
531
532         return data
533 end
534
535 --- Set the gid of a process identified by given pid.
536 -- @param gid   Number containing the Unix group id
537 -- @return              Boolean indicating successful operation
538 -- @return              String containing the error message if failed
539 -- @return              Number containing the error code if failed
540 function process.setgroup(gid)
541         return nixio.setgid(gid)
542 end
543
544 --- Set the uid of a process identified by given pid.
545 -- @param uid   Number containing the Unix user id
546 -- @return              Boolean indicating successful operation
547 -- @return              String containing the error message if failed
548 -- @return              Number containing the error code if failed
549 function process.setuser(uid)
550         return nixio.setuid(uid)
551 end
552
553 --- Send a signal to a process identified by given pid.
554 -- @class function
555 -- @name  process.signal
556 -- @param pid   Number containing the process id
557 -- @param sig   Signal to send (default: 15 [SIGTERM])
558 -- @return              Boolean indicating successful operation
559 -- @return              Number containing the error code if failed
560 process.signal = nixio.kill
561
562
563 --- LuCI system utilities / user related functions.
564 -- @class       module
565 -- @name        luci.sys.user
566 user = {}
567
568 --- Retrieve user informations for given uid.
569 -- @class               function
570 -- @name                getuser
571 -- @param uid   Number containing the Unix user id
572 -- @return              Table containing the following fields:
573 --                              { "uid", "gid", "name", "passwd", "dir", "shell", "gecos" }
574 user.getuser = nixio.getpw
575
576 --- Test whether given string matches the password of a given system user.
577 -- @param username      String containing the Unix user name
578 -- @param pass          String containing the password to compare
579 -- @return                      Boolean indicating wheather the passwords are equal
580 function user.checkpasswd(username, pass)
581         local pwe = nixio.getsp and nixio.getsp(username) or nixio.getpw(username)
582         local pwh = pwe and (pwe.pwdp or pwe.passwd)
583         if not pwh or #pwh < 1 or pwh ~= "!" and nixio.crypt(pass, pwh) ~= pwh then
584                 return false
585         else
586                 return true
587         end
588 end
589
590 --- Change the password of given user.
591 -- @param username      String containing the Unix user name
592 -- @param password      String containing the password to compare
593 -- @return                      Number containing 0 on success and >= 1 on error
594 function user.setpasswd(username, password)
595         if password then
596                 password = password:gsub("'", "")
597         end
598
599         if username then
600                 username = username:gsub("'", "")
601         end
602
603         local cmd = "(echo '"..password.."';sleep 1;echo '"..password.."')|"
604         cmd = cmd .. "passwd '"..username.."' >/dev/null 2>&1"
605         return os.execute(cmd)
606 end
607
608
609 --- LuCI system utilities / wifi related functions.
610 -- @class       module
611 -- @name        luci.sys.wifi
612 wifi = {}
613
614 --- Get iwconfig output for all wireless devices.
615 -- @return      Table of tables containing the iwconfing output for each wifi device
616 function wifi.getiwconfig()
617         local cnt = luci.util.exec("PATH=/sbin:/usr/sbin iwconfig 2>/dev/null")
618         local iwc = {}
619
620         for i, l in pairs(luci.util.split(luci.util.trim(cnt), "\n\n")) do
621                 local k = l:match("^(.-) ")
622                 l = l:gsub("^(.-) +", "", 1)
623                 if k then
624                         local entry, flags = _parse_mixed_record(l)
625                         if entry then
626                                 entry.flags = flags
627                         end
628                         iwc[k] = entry
629                 end
630         end
631
632         return iwc
633 end
634
635 --- Get iwlist scan output from all wireless devices.
636 -- @return      Table of tables contaiing all scan results
637 function wifi.iwscan(iface)
638         local siface = iface or ""
639         local cnt = luci.util.exec("iwlist "..siface.." scan 2>/dev/null")
640         local iws = {}
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("^[^\n]+", "", 1)
645                 l = luci.util.trim(l)
646                 if k then
647                         iws[k] = {}
648                         for j, c in pairs(luci.util.split(l, "\n          Cell")) do
649                                 c = c:gsub("^(.-)- ", "", 1)
650                                 c = luci.util.split(c, "\n", 7)
651                                 c = table.concat(c, "\n", 1)
652                                 local entry, flags = _parse_mixed_record(c)
653                                 if entry then
654                                         entry.flags = flags
655                                 end
656                                 table.insert(iws[k], entry)
657                         end
658                 end
659         end
660
661         return iface and (iws[iface] or {}) or iws
662 end
663
664 --- Get available channels from given wireless iface.
665 -- @param iface Wireless interface (optional)
666 -- @return              Table of available channels
667 function wifi.channels(iface)
668         local cmd = "iwlist " .. ( iface or "" ) .. " freq 2>/dev/null"
669         local cns = { }
670
671         local fd = io.popen(cmd)
672         if fd then
673                 local ln, c, f
674                 while true do
675                         ln = fd:read("*l")
676                         if not ln then break end
677                         c, f = ln:match("Channel (%d+) : (%d+%.%d+) GHz")
678                         if c and f then
679                                 cns[tonumber(c)] = tonumber(f)
680                         end
681                 end
682                 fd:close()
683         end
684
685         if not ((pairs(cns))(cns)) then
686                 cns = {
687                         2.412, 2.417, 2.422, 2.427, 2.432, 2.437,
688                         2.442, 2.447, 2.452, 2.457, 2.462
689                 }
690         end
691
692         return cns
693 end
694
695
696 --- LuCI system utilities / init related functions.
697 -- @class       module
698 -- @name        luci.sys.init
699 init = {}
700 init.dir = "/etc/init.d/"
701
702 --- Get the names of all installed init scripts
703 -- @return      Table containing the names of all inistalled init scripts
704 function init.names()
705         local names = { }
706         for _, name in ipairs(luci.fs.glob(init.dir.."*")) do
707                 names[#names+1] = luci.fs.basename(name)
708         end
709         return names
710 end
711
712 --- Test whether the given init script is enabled
713 -- @param name  Name of the init script
714 -- @return              Boolean indicating whether init is enabled
715 function init.enabled(name)
716         if luci.fs.access(init.dir..name) then
717                 return ( call(init.dir..name.." enabled") == 0 )
718         end
719         return false
720 end
721
722 --- Get the index of he given init script
723 -- @param name  Name of the init script
724 -- @return              Numeric index value
725 function init.index(name)
726         if luci.fs.access(init.dir..name) then
727                 return call("source "..init.dir..name.."; exit $START")
728         end
729 end
730
731 --- Enable the given init script
732 -- @param name  Name of the init script
733 -- @return              Boolean indicating success
734 function init.enable(name)
735         if luci.fs.access(init.dir..name) then
736                 return ( call(init.dir..name.." enable") == 1 )
737         end
738 end
739
740 --- Disable the given init script
741 -- @param name  Name of the init script
742 -- @return              Boolean indicating success
743 function init.disable(name)
744         if luci.fs.access(init.dir..name) then
745                 return ( call(init.dir..name.." disable") == 0 )
746         end
747 end
748
749
750 -- Internal functions
751
752 function _parse_delimited_table(iter, delimiter)
753         delimiter = delimiter or "%s+"
754
755         local data  = {}
756         local trim  = luci.util.trim
757         local split = luci.util.split
758
759         local keys = split(trim(iter()), delimiter, nil, true)
760         for i, j in pairs(keys) do
761                 keys[i] = trim(keys[i])
762         end
763
764         for line in iter do
765                 local row = {}
766                 line = trim(line)
767                 if #line > 0 then
768                         for i, j in pairs(split(line, delimiter, nil, true)) do
769                                 if keys[i] then
770                                         row[keys[i]] = j
771                                 end
772                         end
773                 end
774                 table.insert(data, row)
775         end
776
777         return data
778 end
779
780 function _parse_mixed_record(cnt, delimiter)
781         delimiter = delimiter or "  "
782         local data = {}
783         local flags = {}
784
785         for i, l in pairs(luci.util.split(luci.util.trim(cnt), "\n")) do
786                 for j, f in pairs(luci.util.split(luci.util.trim(l), delimiter, nil, true)) do
787                         local k, x, v = f:match('([^%s][^:=]*) *([:=]*) *"*([^\n"]*)"*')
788
789                         if k then
790                                 if x == "" then
791                                         table.insert(flags, k)
792                                 else
793                                         data[k] = v
794                                 end
795                         end
796                 end
797         end
798
799         return data, flags
800 end