libs/sys: extend luci.sys.hostname() to allow setting a new name
[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 posix = require "posix"
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 = tonumber, ipairs, pairs, pcall
39
40
41 --- LuCI Linux and POSIX system utilities.
42 module "luci.sys"
43
44 --- Execute a given shell command and return the error code
45 -- @class               function
46 -- @name                call
47 -- @param               ...             Command to call
48 -- @return              Error code of the command
49 function call(...)
50         return os.execute(...) / 256
51 end
52
53 --- Execute a given shell command and capture its standard output
54 -- @class               function
55 -- @name                exec
56 -- @param command       Command to call
57 -- @return                      String containg the return the output of the command
58 exec = luci.util.exec
59
60 --- Invoke the luci-flash executable to write an image to the flash memory.
61 -- @param image         Local path or URL to image file
62 -- @param kpattern      Pattern of files to keep over flash process
63 -- @return                      Return value of os.execute()
64 function flash(image, kpattern)
65         local cmd = "luci-flash "
66         if kpattern then
67                 cmd = cmd .. "-k '" .. kpattern:gsub("'", "") .. "' "
68         end
69         cmd = cmd .. "'" .. image:gsub("'", "") .. "' >/dev/null 2>&1"
70
71         return os.execute(cmd)
72 end
73
74 --- Retrieve information about currently mounted file systems.
75 -- @return      Table containing mount information
76 function mounts()
77         local data = {}
78         local k = {"fs", "blocks", "used", "available", "percent", "mountpoint"}
79         local ps = luci.util.execi("df")
80
81         if not ps then
82                 return
83         else
84                 ps()
85         end
86
87         for line in ps do
88                 local row = {}
89
90                 local j = 1
91                 for value in line:gmatch("[^%s]+") do
92                         row[k[j]] = value
93                         j = j + 1
94                 end
95
96                 if row[k[1]] then
97
98                         -- this is a rather ugly workaround to cope with wrapped lines in
99                         -- the df output:
100                         --
101                         --      /dev/scsi/host0/bus0/target0/lun0/part3
102                         --                   114382024  93566472  15005244  86% /mnt/usb
103                         --
104
105                         if not row[k[2]] then
106                                 j = 2
107                                 line = ps()
108                                 for value in line:gmatch("[^%s]+") do
109                                         row[k[j]] = value
110                                         j = j + 1
111                                 end
112                         end
113
114                         table.insert(data, row)
115                 end
116         end
117
118         return data
119 end
120
121 --- Retrieve environment variables. If no variable is given then a table
122 -- containing the whole environment is returned otherwise this function returns
123 -- the corresponding string value for the given name or nil if no such variable
124 -- exists.
125 -- @class               function
126 -- @name                getenv
127 -- @param var   Name of the environment variable to retrieve (optional)
128 -- @return              String containg the value of the specified variable
129 -- @return              Table containing all variables if no variable name is given
130 getenv = posix.getenv
131
132 --- Get or set the current hostname.
133 -- @param               String containing a new hostname to set (optional)
134 -- @return              String containing the system hostname
135 function hostname(newname)
136         if type(newname) == "string" and #newname > 0 then
137                 luci.fs.writefile( "/proc/sys/kernel/hostname", newname .. "\n" )
138                 return newname
139         else
140                 return posix.uname("%n")
141         end
142 end
143
144 --- Returns the contents of a documented referred by an URL.
145 -- @param url    The URL to retrieve
146 -- @param stream Return a stream instead of a buffer
147 -- @param target Directly write to target file name
148 -- @return              String containing the contents of given the URL
149 function httpget(url, stream, target)
150         if not target then
151                 local source = stream and io.popen or luci.util.exec
152                 return source("wget -qO- '"..url:gsub("'", "").."'")
153         else
154                 return os.execute("wget -qO '%s' '%s'" %
155                         {target:gsub("'", ""), url:gsub("'", "")})
156         end
157 end
158
159 --- Returns the system load average values.
160 -- @return      String containing the average load value 1 minute ago
161 -- @return      String containing the average load value 5 minutes ago
162 -- @return      String containing the average load value 15 minutes ago
163 -- @return      String containing the active and total number of processes
164 -- @return      String containing the last used pid
165 function loadavg()
166         local loadavg = io.lines("/proc/loadavg")()
167         return loadavg:match("^(.-) (.-) (.-) (.-) (.-)$")
168 end
169
170 --- Initiate a system reboot.
171 -- @return      Return value of os.execute()
172 function reboot()
173         return os.execute("reboot >/dev/null 2>&1")
174 end
175
176 --- Returns the system type, cpu name and installed physical memory.
177 -- @return      String containing the system or platform identifier
178 -- @return      String containing hardware model information
179 -- @return      String containing the total memory amount in kB
180 -- @return      String containing the memory used for caching in kB
181 -- @return      String containing the memory used for buffering in kB
182 -- @return      String containing the free memory amount in kB
183 function sysinfo()
184         local cpuinfo = luci.fs.readfile("/proc/cpuinfo")
185         local meminfo = luci.fs.readfile("/proc/meminfo")
186
187         local system = cpuinfo:match("system typ.-:%s*([^\n]+)")
188         local model = ""
189         local memtotal = tonumber(meminfo:match("MemTotal:%s*(%d+)"))
190         local memcached = tonumber(meminfo:match("\nCached:%s*(%d+)"))
191         local memfree = tonumber(meminfo:match("MemFree:%s*(%d+)"))
192         local membuffers = tonumber(meminfo:match("Buffers:%s*(%d+)"))
193
194         if not system then
195                 system = posix.uname("%m")
196                 model = cpuinfo:match("model name.-:%s*([^\n]+)")
197                 if not model then
198                         model = cpuinfo:match("Processor.-:%s*([^\n]+)")
199                 end
200         else
201                 model = cpuinfo:match("cpu model.-:%s*([^\n]+)")
202         end
203
204         return system, model, memtotal, memcached, membuffers, memfree
205 end
206
207 --- Retrieves the output of the "logread" command.
208 -- @return      String containing the current log buffer
209 function syslog()
210         return luci.util.exec("logread")
211 end
212
213 --- Retrieves the output of the "dmesg" command.
214 -- @return      String containing the current log buffer
215 function dmesg()
216         return luci.util.exec("dmesg")
217 end
218
219 --- Generates a random id with specified length.
220 -- @param bytes Number of bytes for the unique id
221 -- @return              String containing hex encoded id
222 function uniqueid(bytes)
223         local fp    = io.open("/dev/urandom")
224         local chunk = { fp:read(bytes):byte(1, bytes) }
225         fp:close()
226
227         local hex = ""
228
229         local pattern = "%02X"
230         for i, byte in ipairs(chunk) do
231                 hex = hex .. pattern:format(byte)
232         end
233
234         return hex
235 end
236
237 --- Returns the current system uptime stats.
238 -- @return      String containing total uptime in seconds
239 -- @return      String containing idle time in seconds
240 function uptime()
241         local loadavg = io.lines("/proc/uptime")()
242         return loadavg:match("^(.-) (.-)$")
243 end
244
245 --- LuCI system utilities / POSIX user group related functions.
246 -- @class       module
247 -- @name        luci.sys.group
248 group = {}
249
250 --- Returns information about a POSIX user group.
251 -- @class function
252 -- @name                getgroup
253 -- @param group Group ID or name of a system user group
254 -- @return      Table with information about the requested group
255 group.getgroup = posix.getgroup
256
257
258 --- LuCI system utilities / network related functions.
259 -- @class       module
260 -- @name        luci.sys.net
261 net = {}
262
263 --- Returns the current arp-table entries as two-dimensional table.
264 -- @return      Table of table containing the current arp entries.
265 --                      The following fields are defined for arp entry objects:
266 --                      { "IP address", "HW address", "HW type", "Flags", "Mask", "Device" }
267 function net.arptable()
268         return _parse_delimited_table(io.lines("/proc/net/arp"), "%s%s+")
269 end
270
271 --- Returns conntrack information
272 -- @return      Table with the currently tracked IP connections
273 function net.conntrack()
274         local connt = {}
275         if luci.fs.access("/proc/net/nf_conntrack", "r") then
276                 for line in io.lines("/proc/net/nf_conntrack") do
277                         line = line:match "^(.-( [^ =]+=).-)%2"
278                         local entry, flags = _parse_mixed_record(line, " +")
279                         entry.layer3 = flags[1]
280                         entry.layer4 = flags[3]
281                         for i=1, #entry do
282                                 entry[i] = nil
283                         end
284
285                         connt[#connt+1] = entry
286                 end
287         elseif luci.fs.access("/proc/net/ip_conntrack", "r") then
288                 for line in io.lines("/proc/net/ip_conntrack") do
289                         line = line:match "^(.-( [^ =]+=).-)%2"
290                         local entry, flags = _parse_mixed_record(line, " +")
291                         entry.layer3 = "ipv4"
292                         entry.layer4 = flags[1]
293                         for i=1, #entry do
294                                 entry[i] = nil
295                         end
296
297                         connt[#connt+1] = entry
298                 end
299         else
300                 return nil
301         end
302         return connt
303 end
304
305 --- Determine the current IPv4 default route. If multiple default routes exist,
306 -- return the one with the lowest metric.
307 -- @return      Table with the properties of the current default route.
308 --                      The following fields are defined:
309 --                      { "dest", "gateway", "metric", "refcount", "usecount", "irtt",
310 --                        "flags", "device" }
311 function net.defaultroute()
312         local route = nil
313         for _, r in pairs(net.routes()) do
314                 if r.dest:prefix() == 0 and (not route or route.metric > r.metric) then
315                         route = r
316                 end
317         end
318         return route
319 end
320
321 --- Determine the current IPv6 default route. If multiple default routes exist,
322 -- return the one with the lowest metric.
323 -- @return      Table with the properties of the current default route.
324 --                      The following fields are defined:
325 --                      { "source", "dest", "nexthop", "metric", "refcount", "usecount",
326 --                        "flags", "device" }
327 function net.defaultroute6()
328         local route   = nil
329         local routes6 = net.routes6()
330         if routes6 then
331                 for _, r in pairs(routes6) do
332                         if r.dest:prefix() == 0 and
333                            (not route or route.metric > r.metric)
334                         then
335                                 route = r
336                         end
337                 end
338         end
339         return route
340 end
341
342 --- Determine the names of available network interfaces.
343 -- @return      Table containing all current interface names
344 function net.devices()
345         local devices = {}
346         for line in io.lines("/proc/net/dev") do
347                 table.insert(devices, line:match(" *(.-):"))
348         end
349         return devices
350 end
351
352
353 --- Return information about available network interfaces.
354 -- @return      Table containing all current interface names and their information
355 function net.deviceinfo()
356         local devices = {}
357         for line in io.lines("/proc/net/dev") do
358                 local name, data = line:match("^ *(.-): *(.*)$")
359                 if name and data then
360                         devices[name] = luci.util.split(data, " +", nil, true)
361                 end
362         end
363         return devices
364 end
365
366
367 -- Determine the MAC address belonging to the given IP address.
368 -- @param ip    IPv4 address
369 -- @return              String containing the MAC address or nil if it cannot be found
370 function net.ip4mac(ip)
371         local mac = nil
372
373         for i, l in ipairs(net.arptable()) do
374                 if l["IP address"] == ip then
375                         mac = l["HW address"]
376                 end
377         end
378
379         return mac
380 end
381
382 --- Returns the current kernel routing table entries.
383 -- @return      Table of tables with properties of the corresponding routes.
384 --                      The following fields are defined for route entry tables:
385 --                      { "dest", "gateway", "metric", "refcount", "usecount", "irtt",
386 --                        "flags", "device" }
387 function net.routes()
388         local routes = { }
389
390         for line in io.lines("/proc/net/route") do
391
392                 local dev, dst_ip, gateway, flags, refcnt, usecnt, metric,
393                           dst_mask, mtu, win, irtt = line:match(
394                         "([^%s]+)\t([A-F0-9]+)\t([A-F0-9]+)\t([A-F0-9]+)\t" ..
395                         "(%d+)\t(%d+)\t(%d+)\t([A-F0-9]+)\t(%d+)\t(%d+)\t(%d+)"
396                 )
397
398                 if dev then
399                         gateway  = luci.ip.Hex( gateway,  32, luci.ip.FAMILY_INET4 )
400                         dst_mask = luci.ip.Hex( dst_mask, 32, luci.ip.FAMILY_INET4 )
401                         dst_ip   = luci.ip.Hex(
402                                 dst_ip, dst_mask:prefix(dst_mask), luci.ip.FAMILY_INET4
403                         )
404
405                         routes[#routes+1] = {
406                                 dest     = dst_ip,
407                                 gateway  = gateway,
408                                 metric   = tonumber(metric),
409                                 refcount = tonumber(refcnt),
410                                 usecount = tonumber(usecnt),
411                                 mtu      = tonumber(mtu),
412                                 window   = tonumber(window),
413                                 irtt     = tonumber(irtt),
414                                 flags    = tonumber(flags, 16),
415                                 device   = dev
416                         }
417                 end
418         end
419
420         return routes
421 end
422
423 --- Returns the current ipv6 kernel routing table entries.
424 -- @return      Table of tables with properties of the corresponding routes.
425 --                      The following fields are defined for route entry tables:
426 --                      { "source", "dest", "nexthop", "metric", "refcount", "usecount",
427 --                        "flags", "device" }
428 function net.routes6()
429         if luci.fs.access("/proc/net/ipv6_route", "r") then
430                 local routes = { }
431
432                 for line in io.lines("/proc/net/ipv6_route") do
433
434                         local dst_ip, dst_prefix, src_ip, src_prefix, nexthop,
435                                   metric, refcnt, usecnt, flags, dev = line:match(
436                                 "([a-f0-9]+) ([a-f0-9]+) " ..
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]+) +([^%s]+)"
441                         )
442
443                         src_ip = luci.ip.Hex(
444                                 src_ip, tonumber(src_prefix, 16), luci.ip.FAMILY_INET6, false
445                         )
446
447                         dst_ip = luci.ip.Hex(
448                                 dst_ip, tonumber(dst_prefix, 16), luci.ip.FAMILY_INET6, false
449                         )
450
451                         nexthop = luci.ip.Hex( nexthop, 128, luci.ip.FAMILY_INET6, false )
452
453                         routes[#routes+1] = {
454                                 source   = src_ip,
455                                 dest     = dst_ip,
456                                 nexthop  = nexthop,
457                                 metric   = tonumber(metric, 16),
458                                 refcount = tonumber(refcnt, 16),
459                                 usecount = tonumber(usecnt, 16),
460                                 flags    = tonumber(flags, 16),
461                                 device   = dev
462                         }
463                 end
464
465                 return routes
466         end
467 end
468
469 --- Tests whether the given host responds to ping probes.
470 -- @param host  String containing a hostname or IPv4 address
471 -- @return              Number containing 0 on success and >= 1 on error
472 function net.pingtest(host)
473         return os.execute("ping -c1 '"..host:gsub("'", '').."' >/dev/null 2>&1")
474 end
475
476
477 --- LuCI system utilities / process related functions.
478 -- @class       module
479 -- @name        luci.sys.process
480 process = {}
481
482 --- Get the current process id.
483 -- @class function
484 -- @name  process.info
485 -- @return      Number containing the current pid
486 process.info = posix.getpid
487
488 --- Retrieve information about currently running processes.
489 -- @return      Table containing process information
490 function process.list()
491         local data = {}
492         local k
493         local ps = luci.util.execi("top -bn1")
494
495         if not ps then
496                 return
497         end
498
499         while true do
500                 local line = ps()
501                 if not line then
502                         return
503                 end
504
505                 k = luci.util.split(luci.util.trim(line), "%s+", nil, true)
506                 if k[1] == "PID" then
507                         break
508                 end
509         end
510
511         for line in ps do
512                 local row = {}
513
514                 line = luci.util.trim(line)
515                 for i, value in ipairs(luci.util.split(line, "%s+", #k-1, true)) do
516                         row[k[i]] = value
517                 end
518
519                 local pid = tonumber(row[k[1]])
520                 if pid then
521                         data[pid] = row
522                 end
523         end
524
525         return data
526 end
527
528 --- Set the gid of a process identified by given pid.
529 -- @param pid   Number containing the process id
530 -- @param gid   Number containing the Unix group id
531 -- @return              Boolean indicating successful operation
532 -- @return              String containing the error message if failed
533 -- @return              Number containing the error code if failed
534 function process.setgroup(pid, gid)
535         return posix.setpid("g", pid, gid)
536 end
537
538 --- Set the uid of a process identified by given pid.
539 -- @param pid   Number containing the process id
540 -- @param uid   Number containing the Unix user id
541 -- @return              Boolean indicating successful operation
542 -- @return              String containing the error message if failed
543 -- @return              Number containing the error code if failed
544 function process.setuser(pid, uid)
545         return posix.setpid("u", pid, uid)
546 end
547
548 --- Send a signal to a process identified by given pid.
549 -- @class function
550 -- @name  process.signal
551 -- @param pid   Number containing the process id
552 -- @param sig   Signal to send (default: 15 [SIGTERM])
553 -- @return              Boolean indicating successful operation
554 -- @return              Number containing the error code if failed
555 process.signal = posix.kill
556
557
558 --- LuCI system utilities / user related functions.
559 -- @class       module
560 -- @name        luci.sys.user
561 user = {}
562
563 --- Retrieve user informations for given uid.
564 -- @class               function
565 -- @name                getuser
566 -- @param uid   Number containing the Unix user id
567 -- @return              Table containing the following fields:
568 --                              { "uid", "gid", "name", "passwd", "dir", "shell", "gecos" }
569 user.getuser = posix.getpasswd
570
571 --- Test whether given string matches the password of a given system user.
572 -- @param username      String containing the Unix user name
573 -- @param password      String containing the password to compare
574 -- @return                      Boolean indicating wheather the passwords are equal
575 function user.checkpasswd(username, password)
576         local account = user.getuser(username)
577
578         if account then
579                 local pwd = account.passwd
580                 local shadowpw
581                 if #pwd == 1 then
582                         if luci.fs.stat("/etc/shadow") then
583                                 if not pcall(function()
584                                         for l in io.lines("/etc/shadow") do
585                                                 shadowpw = l:match("^%s:([^:]+)" % username)
586                                                 if shadowpw then
587                                                         pwd = shadowpw
588                                                         break
589                                                 end
590                                         end
591                                 end) then
592                                         return nil, "Unable to access shadow-file"
593                                 end
594                         end
595
596                         if pwd == "!" then
597                                 return true
598                         end
599                 end
600
601                 if pwd and #pwd > 0 and password and #password > 0 then
602                         return (pwd == posix.crypt(password, pwd))
603                 end
604         end
605
606         return false
607 end
608
609 --- Change the password of given user.
610 -- @param username      String containing the Unix user name
611 -- @param password      String containing the password to compare
612 -- @return                      Number containing 0 on success and >= 1 on error
613 function user.setpasswd(username, password)
614         if password then
615                 password = password:gsub("'", "")
616         end
617
618         if username then
619                 username = username:gsub("'", "")
620         end
621
622         local cmd = "(echo '"..password.."';sleep 1;echo '"..password.."')|"
623         cmd = cmd .. "passwd '"..username.."' >/dev/null 2>&1"
624         return os.execute(cmd)
625 end
626
627
628 --- LuCI system utilities / wifi related functions.
629 -- @class       module
630 -- @name        luci.sys.wifi
631 wifi = {}
632
633 --- Get iwconfig output for all wireless devices.
634 -- @return      Table of tables containing the iwconfing output for each wifi device
635 function wifi.getiwconfig()
636         local cnt = luci.util.exec("PATH=/sbin:/usr/sbin iwconfig 2>/dev/null")
637         local iwc = {}
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("^(.-) +", "", 1)
642                 if k then
643                         local entry, flags = _parse_mixed_record(l)
644                         if entry then
645                                 entry.flags = flags
646                         end
647                         iwc[k] = entry
648                 end
649         end
650
651         return iwc
652 end
653
654 --- Get iwlist scan output from all wireless devices.
655 -- @return      Table of tables contaiing all scan results
656 function wifi.iwscan(iface)
657         local siface = iface or ""
658         local cnt = luci.util.exec("iwlist "..siface.." scan 2>/dev/null")
659         local iws = {}
660
661         for i, l in pairs(luci.util.split(luci.util.trim(cnt), "\n\n")) do
662                 local k = l:match("^(.-) ")
663                 l = l:gsub("^[^\n]+", "", 1)
664                 l = luci.util.trim(l)
665                 if k then
666                         iws[k] = {}
667                         for j, c in pairs(luci.util.split(l, "\n          Cell")) do
668                                 c = c:gsub("^(.-)- ", "", 1)
669                                 c = luci.util.split(c, "\n", 7)
670                                 c = table.concat(c, "\n", 1)
671                                 local entry, flags = _parse_mixed_record(c)
672                                 if entry then
673                                         entry.flags = flags
674                                 end
675                                 table.insert(iws[k], entry)
676                         end
677                 end
678         end
679
680         return iface and (iws[iface] or {}) or iws
681 end
682
683
684 --- LuCI system utilities / init related functions.
685 -- @class       module
686 -- @name        luci.sys.init
687 init = {}
688 init.dir = "/etc/init.d/"
689
690 --- Get the names of all installed init scripts
691 -- @return      Table containing the names of all inistalled init scripts
692 function init.names()
693         local names = { }
694         for _, name in ipairs(luci.fs.glob(init.dir.."*")) do
695                 names[#names+1] = luci.fs.basename(name)
696         end
697         return names
698 end
699
700 --- Test whether the given init script is enabled
701 -- @param name  Name of the init script
702 -- @return              Boolean indicating whether init is enabled
703 function init.enabled(name)
704         if luci.fs.access(init.dir..name) then
705                 return ( call(init.dir..name.." enabled") == 0 )
706         end
707         return false
708 end
709
710 --- Get the index of he given init script
711 -- @param name  Name of the init script
712 -- @return              Numeric index value
713 function init.index(name)
714         if luci.fs.access(init.dir..name) then
715                 return call("source "..init.dir..name.."; exit $START")
716         end
717 end
718
719 --- Enable the given init script
720 -- @param name  Name of the init script
721 -- @return              Boolean indicating success
722 function init.enable(name)
723         if luci.fs.access(init.dir..name) then
724                 return ( call(init.dir..name.." enable") == 1 )
725         end
726 end
727
728 --- Disable the given init script
729 -- @param name  Name of the init script
730 -- @return              Boolean indicating success
731 function init.disable(name)
732         if luci.fs.access(init.dir..name) then
733                 return ( call(init.dir..name.." disable") == 0 )
734         end
735 end
736
737
738 -- Internal functions
739
740 function _parse_delimited_table(iter, delimiter)
741         delimiter = delimiter or "%s+"
742
743         local data  = {}
744         local trim  = luci.util.trim
745         local split = luci.util.split
746
747         local keys = split(trim(iter()), delimiter, nil, true)
748         for i, j in pairs(keys) do
749                 keys[i] = trim(keys[i])
750         end
751
752         for line in iter do
753                 local row = {}
754                 line = trim(line)
755                 if #line > 0 then
756                         for i, j in pairs(split(line, delimiter, nil, true)) do
757                                 if keys[i] then
758                                         row[keys[i]] = j
759                                 end
760                         end
761                 end
762                 table.insert(data, row)
763         end
764
765         return data
766 end
767
768 function _parse_mixed_record(cnt, delimiter)
769         delimiter = delimiter or "  "
770         local data = {}
771         local flags = {}
772
773         for i, l in pairs(luci.util.split(luci.util.trim(cnt), "\n")) do
774                 for j, f in pairs(luci.util.split(luci.util.trim(l), delimiter, nil, true)) do
775                         local k, x, v = f:match('([^%s][^:=]*) *([:=]*) *"*([^\n"]*)"*')
776
777                         if k then
778                                 if x == "" then
779                                         table.insert(flags, k)
780                                 else
781                                         data[k] = v
782                                 end
783                         end
784                 end
785         end
786
787         return data, flags
788 end