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