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