d6cc6346bcfed59d7039723da572f1d9ffb67e18
[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 = nil
306 local routes6 = net.routes6()
307 if routes6 then
308 for _, r in pairs(routes6) do
309 if r.dest:prefix() == 0 and
310 (not route or route.metric > r.metric)
311 then
312 route = r
313 end
314 end
315 end
316 return route
317 end
318
319 --- Determine the names of available network interfaces.
320 -- @return Table containing all current interface names
321 function net.devices()
322 local devs = {}
323 for k, v in ipairs(nixio.getifaddrs()) do
324 if v.family == "packet" then
325 devs[#devs+1] = v.name
326 end
327 end
328 return devs
329 end
330
331
332 --- Return information about available network interfaces.
333 -- @return Table containing all current interface names and their information
334 function net.deviceinfo()
335 local devs = {}
336 for k, v in ipairs(nixio.getifaddrs()) do
337 if v.family == "packet" then
338 local d = v.data
339 d[1] = d.rx_bytes
340 d[2] = d.rx_packets
341 d[3] = d.rx_errors
342 d[4] = d.rx_dropped
343 d[5] = 0
344 d[6] = 0
345 d[7] = 0
346 d[8] = d.multicast
347 d[9] = d.tx_bytes
348 d[10] = d.tx_packets
349 d[11] = d.tx_errors
350 d[12] = d.tx_dropped
351 d[13] = 0
352 d[14] = d.collisions
353 d[15] = 0
354 d[16] = 0
355 devs[v.name] = d
356 end
357 end
358 return devs
359 end
360
361
362 -- Determine the MAC address belonging to the given IP address.
363 -- @param ip IPv4 address
364 -- @return String containing the MAC address or nil if it cannot be found
365 function net.ip4mac(ip)
366 local mac = nil
367
368 for i, l in ipairs(net.arptable()) do
369 if l["IP address"] == ip then
370 mac = l["HW address"]
371 end
372 end
373
374 return mac
375 end
376
377 --- Returns the current kernel routing table entries.
378 -- @return Table of tables with properties of the corresponding routes.
379 -- The following fields are defined for route entry tables:
380 -- { "dest", "gateway", "metric", "refcount", "usecount", "irtt",
381 -- "flags", "device" }
382 function net.routes(callback)
383 local routes = { }
384
385 for line in io.lines("/proc/net/route") do
386
387 local dev, dst_ip, gateway, flags, refcnt, usecnt, metric,
388 dst_mask, mtu, win, irtt = line:match(
389 "([^%s]+)\t([A-F0-9]+)\t([A-F0-9]+)\t([A-F0-9]+)\t" ..
390 "(%d+)\t(%d+)\t(%d+)\t([A-F0-9]+)\t(%d+)\t(%d+)\t(%d+)"
391 )
392
393 if dev then
394 gateway = luci.ip.Hex( gateway, 32, luci.ip.FAMILY_INET4 )
395 dst_mask = luci.ip.Hex( dst_mask, 32, luci.ip.FAMILY_INET4 )
396 dst_ip = luci.ip.Hex(
397 dst_ip, dst_mask:prefix(dst_mask), luci.ip.FAMILY_INET4
398 )
399
400 local rt = {
401 dest = dst_ip,
402 gateway = gateway,
403 metric = tonumber(metric),
404 refcount = tonumber(refcnt),
405 usecount = tonumber(usecnt),
406 mtu = tonumber(mtu),
407 window = tonumber(window),
408 irtt = tonumber(irtt),
409 flags = tonumber(flags, 16),
410 device = dev
411 }
412
413 if callback then
414 callback(rt)
415 else
416 routes[#routes+1] = rt
417 end
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 function process.info(key)
488 local s = {uid = nixio.getuid(), gid = nixio.getgid()}
489 return not key and s or s[key]
490 end
491
492 --- Retrieve information about currently running processes.
493 -- @return Table containing process information
494 function process.list()
495 local data = {}
496 local k
497 local ps = luci.util.execi("top -bn1")
498
499 if not ps then
500 return
501 end
502
503 while true do
504 local line = ps()
505 if not line then
506 return
507 end
508
509 k = luci.util.split(luci.util.trim(line), "%s+", nil, true)
510 if k[1] == "PID" then
511 break
512 end
513 end
514
515 for line in ps do
516 local row = {}
517
518 line = luci.util.trim(line)
519 for i, value in ipairs(luci.util.split(line, "%s+", #k-1, true)) do
520 row[k[i]] = value
521 end
522
523 local pid = tonumber(row[k[1]])
524 if pid then
525 data[pid] = row
526 end
527 end
528
529 return data
530 end
531
532 --- Set the gid of a process identified by given pid.
533 -- @param gid Number containing the Unix group id
534 -- @return Boolean indicating successful operation
535 -- @return String containing the error message if failed
536 -- @return Number containing the error code if failed
537 function process.setgroup(gid)
538 return nixio.setgid(gid)
539 end
540
541 --- Set the uid of a process identified by given pid.
542 -- @param uid Number containing the Unix user id
543 -- @return Boolean indicating successful operation
544 -- @return String containing the error message if failed
545 -- @return Number containing the error code if failed
546 function process.setuser(uid)
547 return nixio.setuid(uid)
548 end
549
550 --- Send a signal to a process identified by given pid.
551 -- @class function
552 -- @name process.signal
553 -- @param pid Number containing the process id
554 -- @param sig Signal to send (default: 15 [SIGTERM])
555 -- @return Boolean indicating successful operation
556 -- @return Number containing the error code if failed
557 process.signal = nixio.kill
558
559
560 --- LuCI system utilities / user related functions.
561 -- @class module
562 -- @name luci.sys.user
563 user = {}
564
565 --- Retrieve user informations for given uid.
566 -- @class function
567 -- @name getuser
568 -- @param uid Number containing the Unix user id
569 -- @return Table containing the following fields:
570 -- { "uid", "gid", "name", "passwd", "dir", "shell", "gecos" }
571 user.getuser = nixio.getpw
572
573 --- Test whether given string matches the password of a given system user.
574 -- @param username String containing the Unix user name
575 -- @param pass String containing the password to compare
576 -- @return Boolean indicating wheather the passwords are equal
577 function user.checkpasswd(username, pass)
578 local pwe = nixio.getsp and nixio.getsp(username) or nixio.getpw(username)
579 local pwh = pwe and (pwe.pwdp or pwe.passwd)
580 if not pwh or #pwh < 1 or pwh ~= "!" and nixio.crypt(pass, pwh) ~= pwh then
581 return false
582 else
583 return true
584 end
585 end
586
587 --- Change the password of given user.
588 -- @param username String containing the Unix user name
589 -- @param password String containing the password to compare
590 -- @return Number containing 0 on success and >= 1 on error
591 function user.setpasswd(username, password)
592 if password then
593 password = password:gsub("'", "")
594 end
595
596 if username then
597 username = username:gsub("'", "")
598 end
599
600 local cmd = "(echo '"..password.."';sleep 1;echo '"..password.."')|"
601 cmd = cmd .. "passwd '"..username.."' >/dev/null 2>&1"
602 return os.execute(cmd)
603 end
604
605
606 --- LuCI system utilities / wifi related functions.
607 -- @class module
608 -- @name luci.sys.wifi
609 wifi = {}
610
611 --- Get iwconfig output for all wireless devices.
612 -- @return Table of tables containing the iwconfing output for each wifi device
613 function wifi.getiwconfig()
614 local cnt = luci.util.exec("PATH=/sbin:/usr/sbin iwconfig 2>/dev/null")
615 local iwc = {}
616
617 for i, l in pairs(luci.util.split(luci.util.trim(cnt), "\n\n")) do
618 local k = l:match("^(.-) ")
619 l = l:gsub("^(.-) +", "", 1)
620 if k then
621 local entry, flags = _parse_mixed_record(l)
622 if entry then
623 entry.flags = flags
624 end
625 iwc[k] = entry
626 end
627 end
628
629 return iwc
630 end
631
632 --- Get iwlist scan output from all wireless devices.
633 -- @return Table of tables contaiing all scan results
634 function wifi.iwscan(iface)
635 local siface = iface or ""
636 local cnt = luci.util.exec("iwlist "..siface.." scan 2>/dev/null")
637 local iws = {}
638
639 for i, l in pairs(luci.util.split(luci.util.trim(cnt), "\n\n")) do
640 local k = l:match("^(.-) ")
641 l = l:gsub("^[^\n]+", "", 1)
642 l = luci.util.trim(l)
643 if k then
644 iws[k] = {}
645 for j, c in pairs(luci.util.split(l, "\n Cell")) do
646 c = c:gsub("^(.-)- ", "", 1)
647 c = luci.util.split(c, "\n", 7)
648 c = table.concat(c, "\n", 1)
649 local entry, flags = _parse_mixed_record(c)
650 if entry then
651 entry.flags = flags
652 end
653 table.insert(iws[k], entry)
654 end
655 end
656 end
657
658 return iface and (iws[iface] or {}) or iws
659 end
660
661 --- Get available channels from given wireless iface.
662 -- @param iface Wireless interface (optional)
663 -- @return Table of available channels
664 function wifi.channels(iface)
665 local cmd = "iwlist " .. ( iface or "" ) .. " freq 2>/dev/null"
666 local cns = { }
667
668 local fd = io.popen(cmd)
669 if fd then
670 local ln, c, f
671 while true do
672 ln = fd:read("*l")
673 if not ln then break end
674 c, f = ln:match("Channel (%d+) : (%d+%.%d+) GHz")
675 if c and f then
676 cns[tonumber(c)] = tonumber(f)
677 end
678 end
679 fd:close()
680 end
681
682 if not ((pairs(cns))(cns)) then
683 cns = {
684 2.412, 2.417, 2.422, 2.427, 2.432, 2.437,
685 2.442, 2.447, 2.452, 2.457, 2.462
686 }
687 end
688
689 return cns
690 end
691
692
693 --- LuCI system utilities / init related functions.
694 -- @class module
695 -- @name luci.sys.init
696 init = {}
697 init.dir = "/etc/init.d/"
698
699 --- Get the names of all installed init scripts
700 -- @return Table containing the names of all inistalled init scripts
701 function init.names()
702 local names = { }
703 for _, name in ipairs(luci.fs.glob(init.dir.."*")) do
704 names[#names+1] = luci.fs.basename(name)
705 end
706 return names
707 end
708
709 --- Test whether the given init script is enabled
710 -- @param name Name of the init script
711 -- @return Boolean indicating whether init is enabled
712 function init.enabled(name)
713 if luci.fs.access(init.dir..name) then
714 return ( call(init.dir..name.." enabled") == 0 )
715 end
716 return false
717 end
718
719 --- Get the index of he given init script
720 -- @param name Name of the init script
721 -- @return Numeric index value
722 function init.index(name)
723 if luci.fs.access(init.dir..name) then
724 return call("source "..init.dir..name.."; exit $START")
725 end
726 end
727
728 --- Enable the given init script
729 -- @param name Name of the init script
730 -- @return Boolean indicating success
731 function init.enable(name)
732 if luci.fs.access(init.dir..name) then
733 return ( call(init.dir..name.." enable") == 1 )
734 end
735 end
736
737 --- Disable the given init script
738 -- @param name Name of the init script
739 -- @return Boolean indicating success
740 function init.disable(name)
741 if luci.fs.access(init.dir..name) then
742 return ( call(init.dir..name.." disable") == 0 )
743 end
744 end
745
746
747 -- Internal functions
748
749 function _parse_delimited_table(iter, delimiter)
750 delimiter = delimiter or "%s+"
751
752 local data = {}
753 local trim = luci.util.trim
754 local split = luci.util.split
755
756 local keys = split(trim(iter()), delimiter, nil, true)
757 for i, j in pairs(keys) do
758 keys[i] = trim(keys[i])
759 end
760
761 for line in iter do
762 local row = {}
763 line = trim(line)
764 if #line > 0 then
765 for i, j in pairs(split(line, delimiter, nil, true)) do
766 if keys[i] then
767 row[keys[i]] = j
768 end
769 end
770 end
771 table.insert(data, row)
772 end
773
774 return data
775 end
776
777 function _parse_mixed_record(cnt, delimiter)
778 delimiter = delimiter or " "
779 local data = {}
780 local flags = {}
781
782 for i, l in pairs(luci.util.split(luci.util.trim(cnt), "\n")) do
783 for j, f in pairs(luci.util.split(luci.util.trim(l), delimiter, nil, true)) do
784 local k, x, v = f:match('([^%s][^:=]*) *([:=]*) *"*([^\n"]*)"*')
785
786 if k then
787 if x == "" then
788 table.insert(flags, k)
789 else
790 data[k] = v
791 end
792 end
793 end
794 end
795
796 return data, flags
797 end