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