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