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