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