Don't fail if we do not have v6 support
[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 not routes6 then
325 return nil
326 end
327 for _, r in pairs(routes6) do
328 if r.dest:prefix() == 0 and (not route or route.metric > r.metric) then
329 route = r
330 end
331 end
332 return route
333 end
334
335 --- Determine the names of available network interfaces.
336 -- @return Table containing all current interface names
337 function net.devices()
338 local devices = {}
339 for line in io.lines("/proc/net/dev") do
340 table.insert(devices, line:match(" *(.-):"))
341 end
342 return devices
343 end
344
345
346 --- Return information about available network interfaces.
347 -- @return Table containing all current interface names and their information
348 function net.deviceinfo()
349 local devices = {}
350 for line in io.lines("/proc/net/dev") do
351 local name, data = line:match("^ *(.-): *(.*)$")
352 if name and data then
353 devices[name] = luci.util.split(data, " +", nil, true)
354 end
355 end
356 return devices
357 end
358
359
360 -- Determine the MAC address belonging to the given IP address.
361 -- @param ip IPv4 address
362 -- @return String containing the MAC address or nil if it cannot be found
363 function net.ip4mac(ip)
364 local mac = nil
365
366 for i, l in ipairs(net.arptable()) do
367 if l["IP address"] == ip then
368 mac = l["HW address"]
369 end
370 end
371
372 return mac
373 end
374
375 --- Returns the current kernel routing table entries.
376 -- @return Table of tables with properties of the corresponding routes.
377 -- The following fields are defined for route entry tables:
378 -- { "dest", "gateway", "metric", "refcount", "usecount", "irtt",
379 -- "flags", "device" }
380 function net.routes()
381 local routes = { }
382
383 for line in io.lines("/proc/net/route") do
384
385 local dev, dst_ip, gateway, flags, refcnt, usecnt, metric,
386 dst_mask, mtu, win, irtt = line:match(
387 "([^%s]+)\t([A-F0-9]+)\t([A-F0-9]+)\t([A-F0-9]+)\t" ..
388 "(%d+)\t(%d+)\t(%d+)\t([A-F0-9]+)\t(%d+)\t(%d+)\t(%d+)"
389 )
390
391 if dev then
392 gateway = luci.ip.Hex( gateway, 32, luci.ip.FAMILY_INET4 )
393 dst_mask = luci.ip.Hex( dst_mask, 32, luci.ip.FAMILY_INET4 )
394 dst_ip = luci.ip.Hex(
395 dst_ip, dst_mask:prefix(dst_mask), luci.ip.FAMILY_INET4
396 )
397
398 routes[#routes+1] = {
399 dest = dst_ip,
400 gateway = gateway,
401 metric = tonumber(metric),
402 refcount = tonumber(refcnt),
403 usecount = tonumber(usecnt),
404 mtu = tonumber(mtu),
405 window = tonumber(window),
406 irtt = tonumber(irtt),
407 flags = tonumber(flags, 16),
408 device = dev
409 }
410 end
411 end
412
413 return routes
414 end
415
416 --- Returns the current ipv6 kernel routing table entries.
417 -- @return Table of tables with properties of the corresponding routes.
418 -- The following fields are defined for route entry tables:
419 -- { "source", "dest", "nexthop", "metric", "refcount", "usecount",
420 -- "flags", "device" }
421 function net.routes6()
422 local routes = { }
423
424 if not luci.fs.access("/proc/net/ipv6_route", "r") then
425 return nil
426 end
427
428 for line in io.lines("/proc/net/ipv6_route") do
429
430 local dst_ip, dst_prefix, src_ip, src_prefix, nexthop,
431 metric, refcnt, usecnt, flags, dev = line:match(
432 "([a-f0-9]+) ([a-f0-9]+) " ..
433 "([a-f0-9]+) ([a-f0-9]+) " ..
434 "([a-f0-9]+) ([a-f0-9]+) " ..
435 "([a-f0-9]+) ([a-f0-9]+) " ..
436 "([a-f0-9]+) +([^%s]+)"
437 )
438
439 src_ip = luci.ip.Hex(
440 src_ip, tonumber(src_prefix, 16), luci.ip.FAMILY_INET6, false
441 )
442
443 dst_ip = luci.ip.Hex(
444 dst_ip, tonumber(dst_prefix, 16), luci.ip.FAMILY_INET6, false
445 )
446
447 nexthop = luci.ip.Hex( nexthop, 128, luci.ip.FAMILY_INET6, false )
448
449 routes[#routes+1] = {
450 source = src_ip,
451 dest = dst_ip,
452 nexthop = nexthop,
453 metric = tonumber(metric, 16),
454 refcount = tonumber(refcnt, 16),
455 usecount = tonumber(usecnt, 16),
456 flags = tonumber(flags, 16),
457 device = dev
458 }
459 end
460
461 return routes
462 end
463
464 --- Tests whether the given host responds to ping probes.
465 -- @param host String containing a hostname or IPv4 address
466 -- @return Number containing 0 on success and >= 1 on error
467 function net.pingtest(host)
468 return os.execute("ping -c1 '"..host:gsub("'", '').."' >/dev/null 2>&1")
469 end
470
471
472 --- LuCI system utilities / process related functions.
473 -- @class module
474 -- @name luci.sys.process
475 process = {}
476
477 --- Get the current process id.
478 -- @class function
479 -- @name process.info
480 -- @return Number containing the current pid
481 process.info = posix.getpid
482
483 --- Retrieve information about currently running processes.
484 -- @return Table containing process information
485 function process.list()
486 local data = {}
487 local k
488 local ps = luci.util.execi("top -bn1")
489
490 if not ps then
491 return
492 end
493
494 while true do
495 local line = ps()
496 if not line then
497 return
498 end
499
500 k = luci.util.split(luci.util.trim(line), "%s+", nil, true)
501 if k[1] == "PID" then
502 break
503 end
504 end
505
506 for line in ps do
507 local row = {}
508
509 line = luci.util.trim(line)
510 for i, value in ipairs(luci.util.split(line, "%s+", #k-1, true)) do
511 row[k[i]] = value
512 end
513
514 local pid = tonumber(row[k[1]])
515 if pid then
516 data[pid] = row
517 end
518 end
519
520 return data
521 end
522
523 --- Set the gid of a process identified by given pid.
524 -- @param pid Number containing the process id
525 -- @param gid Number containing the Unix group id
526 -- @return Boolean indicating successful operation
527 -- @return String containing the error message if failed
528 -- @return Number containing the error code if failed
529 function process.setgroup(pid, gid)
530 return posix.setpid("g", pid, gid)
531 end
532
533 --- Set the uid of a process identified by given pid.
534 -- @param pid Number containing the process id
535 -- @param uid Number containing the Unix user id
536 -- @return Boolean indicating successful operation
537 -- @return String containing the error message if failed
538 -- @return Number containing the error code if failed
539 function process.setuser(pid, uid)
540 return posix.setpid("u", pid, uid)
541 end
542
543 --- Send a signal to a process identified by given pid.
544 -- @class function
545 -- @name process.signal
546 -- @param pid Number containing the process id
547 -- @param sig Signal to send (default: 15 [SIGTERM])
548 -- @return Boolean indicating successful operation
549 -- @return Number containing the error code if failed
550 process.signal = posix.kill
551
552
553 --- LuCI system utilities / user related functions.
554 -- @class module
555 -- @name luci.sys.user
556 user = {}
557
558 --- Retrieve user informations for given uid.
559 -- @class function
560 -- @name getuser
561 -- @param uid Number containing the Unix user id
562 -- @return Table containing the following fields:
563 -- { "uid", "gid", "name", "passwd", "dir", "shell", "gecos" }
564 user.getuser = posix.getpasswd
565
566 --- Test whether given string matches the password of a given system user.
567 -- @param username String containing the Unix user name
568 -- @param password String containing the password to compare
569 -- @return Boolean indicating wheather the passwords are equal
570 function user.checkpasswd(username, password)
571 local account = user.getuser(username)
572
573 if account then
574 local pwd = account.passwd
575 local shadowpw
576 if #pwd == 1 then
577 if luci.fs.stat("/etc/shadow") then
578 if not pcall(function()
579 for l in io.lines("/etc/shadow") do
580 shadowpw = l:match("^%s:([^:]+)" % username)
581 if shadowpw then
582 pwd = shadowpw
583 break
584 end
585 end
586 end) then
587 return nil, "Unable to access shadow-file"
588 end
589 end
590
591 if pwd == "!" then
592 return true
593 end
594 end
595
596 if pwd and #pwd > 0 and password and #password > 0 then
597 return (pwd == posix.crypt(password, pwd))
598 end
599 end
600
601 return false
602 end
603
604 --- Change the password of given user.
605 -- @param username String containing the Unix user name
606 -- @param password String containing the password to compare
607 -- @return Number containing 0 on success and >= 1 on error
608 function user.setpasswd(username, password)
609 if password then
610 password = password:gsub("'", "")
611 end
612
613 if username then
614 username = username:gsub("'", "")
615 end
616
617 local cmd = "(echo '"..password.."';sleep 1;echo '"..password.."')|"
618 cmd = cmd .. "passwd '"..username.."' >/dev/null 2>&1"
619 return os.execute(cmd)
620 end
621
622
623 --- LuCI system utilities / wifi related functions.
624 -- @class module
625 -- @name luci.sys.wifi
626 wifi = {}
627
628 --- Get iwconfig output for all wireless devices.
629 -- @return Table of tables containing the iwconfing output for each wifi device
630 function wifi.getiwconfig()
631 local cnt = luci.util.exec("/usr/sbin/iwconfig 2>/dev/null")
632 local iwc = {}
633
634 for i, l in pairs(luci.util.split(luci.util.trim(cnt), "\n\n")) do
635 local k = l:match("^(.-) ")
636 l = l:gsub("^(.-) +", "", 1)
637 if k then
638 local entry, flags = _parse_mixed_record(l)
639 if entry then
640 entry.flags = flags
641 end
642 iwc[k] = entry
643 end
644 end
645
646 return iwc
647 end
648
649 --- Get iwlist scan output from all wireless devices.
650 -- @return Table of tables contaiing all scan results
651 function wifi.iwscan(iface)
652 local siface = iface or ""
653 local cnt = luci.util.exec("iwlist "..siface.." scan 2>/dev/null")
654 local iws = {}
655
656 for i, l in pairs(luci.util.split(luci.util.trim(cnt), "\n\n")) do
657 local k = l:match("^(.-) ")
658 l = l:gsub("^[^\n]+", "", 1)
659 l = luci.util.trim(l)
660 if k then
661 iws[k] = {}
662 for j, c in pairs(luci.util.split(l, "\n Cell")) do
663 c = c:gsub("^(.-)- ", "", 1)
664 c = luci.util.split(c, "\n", 7)
665 c = table.concat(c, "\n", 1)
666 local entry, flags = _parse_mixed_record(c)
667 if entry then
668 entry.flags = flags
669 end
670 table.insert(iws[k], entry)
671 end
672 end
673 end
674
675 return iface and (iws[iface] or {}) or iws
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