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