b7e374d7cf613ee2c990cac96b30220d4f133ca3
[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") then
270 for line in io.lines("/proc/net/nf_conntrack") do
271 local entry, flags = _parse_mixed_record(line, " +")
272 entry.layer3 = flags[1]
273 entry.layer4 = flags[2]
274 for i=1, #entry do
275 entry[i] = nil
276 end
277
278 connt[#connt+1] = entry
279 end
280 elseif luci.fs.access("/proc/net/ip_conntrack") then
281 for line in io.lines("/proc/net/ip_conntrack") do
282 local entry, flags = _parse_mixed_record(line, " +")
283 entry.layer3 = "ipv4"
284 entry.layer4 = flags[1]
285 for i=1, #entry do
286 entry[i] = nil
287 end
288
289 connt[#connt+1] = entry
290 end
291 else
292 return nil
293 end
294 return connt
295 end
296
297 --- Determine the current default route.
298 -- @return Table with the properties of the current default route.
299 -- The following fields are defined:
300 -- { "Mask", "RefCnt", "Iface", "Flags", "Window", "IRTT",
301 -- "MTU", "Gateway", "Destination", "Metric", "Use" }
302 function net.defaultroute()
303 local routes = net.routes()
304 local route = nil
305
306 for i, r in pairs(luci.sys.net.routes()) do
307 if r.Destination == "00000000" and (not route or route.Metric > r.Metric) then
308 route = r
309 end
310 end
311
312 return route
313 end
314
315 --- Determine the names of available network interfaces.
316 -- @return Table containing all current interface names
317 function net.devices()
318 local devices = {}
319 for line in io.lines("/proc/net/dev") do
320 table.insert(devices, line:match(" *(.-):"))
321 end
322 return devices
323 end
324
325
326 --- Return information about available network interfaces.
327 -- @return Table containing all current interface names and their information
328 function net.deviceinfo()
329 local devices = {}
330 for line in io.lines("/proc/net/dev") do
331 local name, data = line:match("^ *(.-): *(.*)$")
332 if name and data then
333 devices[name] = luci.util.split(data, " +", nil, true)
334 end
335 end
336 return devices
337 end
338
339
340 -- Determine the MAC address belonging to the given IP address.
341 -- @param ip IPv4 address
342 -- @return String containing the MAC address or nil if it cannot be found
343 function net.ip4mac(ip)
344 local mac = nil
345
346 for i, l in ipairs(net.arptable()) do
347 if l["IP address"] == ip then
348 mac = l["HW address"]
349 end
350 end
351
352 return mac
353 end
354
355 --- Returns the current kernel routing table entries.
356 -- @return Table of tables with properties of the corresponding routes.
357 -- The following fields are defined for route entry tables:
358 -- { "Mask", "RefCnt", "Iface", "Flags", "Window", "IRTT",
359 -- "MTU", "Gateway", "Destination", "Metric", "Use" }
360 function net.routes()
361 return _parse_delimited_table(io.lines("/proc/net/route"))
362 end
363
364 --- Returns the current ipv6 kernel routing table entries.
365 -- @return Table of tables with properties of the corresponding routes.
366 -- The following fields are defined for route entry tables:
367 -- { "src_ip", "src_prefix", "dst_ip", "dst_prefix", "nexthop_ip",
368 -- "metric", "refcount", "usecount", "flags", "device" }
369 function net.routes6()
370 local routes = { }
371
372 for line in io.lines("/proc/net/ipv6_route") do
373
374 local dst_ip, dst_prefix, src_ip, src_prefix, nexthop,
375 metric, refcnt, usecnt, flags, dev = line:match(
376 "([a-f0-9]+) ([a-f0-9]+) " ..
377 "([a-f0-9]+) ([a-f0-9]+) " ..
378 "([a-f0-9]+) ([a-f0-9]+) " ..
379 "([a-f0-9]+) ([a-f0-9]+) " ..
380 "([^%s]+) +([^%s]+)"
381 )
382
383 src_ip = luci.ip.Hex(
384 src_ip, tonumber(src_prefix, 16),
385 luci.ip.FAMILY_INET6, false
386 )
387
388 dst_ip = luci.ip.Hex(
389 dst_ip, tonumber(dst_prefix, 16),
390 luci.ip.FAMILY_INET6, false
391 )
392
393 nexthop = luci.ip.Hex( nexthop, 128, luci.ip.FAMILY_INET6, false )
394
395 routes[#routes+1] = {
396 src_ip = src_ip:host():string(),
397 src_prefix = src_ip:prefix(),
398 dst_ip = dst_ip:host():string(),
399 dst_prefix = dst_ip:prefix(),
400 nexthop_ip = nexthop:string(),
401 metric = tonumber(metric, 16),
402 refcount = tonumber(refcnt, 16),
403 usecount = tonumber(usecnt, 16),
404 flags = tonumber(flags), -- hex?
405 device = dev
406 }
407 end
408
409 return routes
410 end
411
412 --- Tests whether the given host responds to ping probes.
413 -- @param host String containing a hostname or IPv4 address
414 -- @return Number containing 0 on success and >= 1 on error
415 function net.pingtest(host)
416 return os.execute("ping -c1 '"..host:gsub("'", '').."' >/dev/null 2>&1")
417 end
418
419
420 --- LuCI system utilities / process related functions.
421 -- @class module
422 -- @name luci.sys.process
423 process = {}
424
425 --- Get the current process id.
426 -- @class function
427 -- @name process.info
428 -- @return Number containing the current pid
429 process.info = posix.getpid
430
431 --- Retrieve information about currently running processes.
432 -- @return Table containing process information
433 function process.list()
434 local data = {}
435 local k
436 local ps = luci.util.execi("top -bn1")
437
438 if not ps then
439 return
440 end
441
442 while true do
443 local line = ps()
444 if not line then
445 return
446 end
447
448 k = luci.util.split(luci.util.trim(line), "%s+", nil, true)
449 if k[1] == "PID" then
450 break
451 end
452 end
453
454 for line in ps do
455 local row = {}
456
457 line = luci.util.trim(line)
458 for i, value in ipairs(luci.util.split(line, "%s+", #k-1, true)) do
459 row[k[i]] = value
460 end
461
462 local pid = tonumber(row[k[1]])
463 if pid then
464 data[pid] = row
465 end
466 end
467
468 return data
469 end
470
471 --- Set the gid of a process identified by given pid.
472 -- @param pid Number containing the process id
473 -- @param gid Number containing the Unix group id
474 -- @return Boolean indicating successful operation
475 -- @return String containing the error message if failed
476 -- @return Number containing the error code if failed
477 function process.setgroup(pid, gid)
478 return posix.setpid("g", pid, gid)
479 end
480
481 --- Set the uid of a process identified by given pid.
482 -- @param pid Number containing the process id
483 -- @param uid Number containing the Unix user id
484 -- @return Boolean indicating successful operation
485 -- @return String containing the error message if failed
486 -- @return Number containing the error code if failed
487 function process.setuser(pid, uid)
488 return posix.setpid("u", pid, uid)
489 end
490
491 --- Send a signal to a process identified by given pid.
492 -- @class function
493 -- @name process.signal
494 -- @param pid Number containing the process id
495 -- @param sig Signal to send (default: 15 [SIGTERM])
496 -- @return Boolean indicating successful operation
497 -- @return Number containing the error code if failed
498 process.signal = posix.kill
499
500
501 --- LuCI system utilities / user related functions.
502 -- @class module
503 -- @name luci.sys.user
504 user = {}
505
506 --- Retrieve user informations for given uid.
507 -- @class function
508 -- @name getuser
509 -- @param uid Number containing the Unix user id
510 -- @return Table containing the following fields:
511 -- { "uid", "gid", "name", "passwd", "dir", "shell", "gecos" }
512 user.getuser = posix.getpasswd
513
514 --- Test whether given string matches the password of a given system user.
515 -- @param username String containing the Unix user name
516 -- @param password String containing the password to compare
517 -- @return Boolean indicating wheather the passwords are equal
518 function user.checkpasswd(username, password)
519 local account = user.getuser(username)
520
521 if account then
522 local pwd = account.passwd
523 local shadowpw
524 if #pwd == 1 then
525 if luci.fs.stat("/etc/shadow") then
526 if not pcall(function()
527 for l in io.lines("/etc/shadow") do
528 shadowpw = l:match("^%s:([^:]+)" % username)
529 if shadowpw then
530 pwd = shadowpw
531 break
532 end
533 end
534 end) then
535 return nil, "Unable to access shadow-file"
536 end
537 end
538
539 if pwd == "!" then
540 return true
541 end
542 end
543
544 if pwd and #pwd > 0 and password and #password > 0 then
545 return (pwd == posix.crypt(password, pwd))
546 end
547 end
548
549 return false
550 end
551
552 --- Change the password of given user.
553 -- @param username String containing the Unix user name
554 -- @param password String containing the password to compare
555 -- @return Number containing 0 on success and >= 1 on error
556 function user.setpasswd(username, password)
557 if password then
558 password = password:gsub("'", "")
559 end
560
561 if username then
562 username = username:gsub("'", "")
563 end
564
565 local cmd = "(echo '"..password.."';sleep 1;echo '"..password.."')|"
566 cmd = cmd .. "passwd '"..username.."' >/dev/null 2>&1"
567 return os.execute(cmd)
568 end
569
570
571 --- LuCI system utilities / wifi related functions.
572 -- @class module
573 -- @name luci.sys.wifi
574 wifi = {}
575
576 --- Get iwconfig output for all wireless devices.
577 -- @return Table of tables containing the iwconfing output for each wifi device
578 function wifi.getiwconfig()
579 local cnt = luci.util.exec("/usr/sbin/iwconfig 2>/dev/null")
580 local iwc = {}
581
582 for i, l in pairs(luci.util.split(luci.util.trim(cnt), "\n\n")) do
583 local k = l:match("^(.-) ")
584 l = l:gsub("^(.-) +", "", 1)
585 if k then
586 local entry, flags = _parse_mixed_record(l)
587 if entry then
588 entry.flags = flags
589 end
590 iwc[k] = entry
591 end
592 end
593
594 return iwc
595 end
596
597 --- Get iwlist scan output from all wireless devices.
598 -- @return Table of tables contaiing all scan results
599 function wifi.iwscan(iface)
600 local siface = iface or ""
601 local cnt = luci.util.exec("iwlist "..siface.." scan 2>/dev/null")
602 local iws = {}
603
604 for i, l in pairs(luci.util.split(luci.util.trim(cnt), "\n\n")) do
605 local k = l:match("^(.-) ")
606 l = l:gsub("^[^\n]+", "", 1)
607 l = luci.util.trim(l)
608 if k then
609 iws[k] = {}
610 for j, c in pairs(luci.util.split(l, "\n Cell")) do
611 c = c:gsub("^(.-)- ", "", 1)
612 c = luci.util.split(c, "\n", 7)
613 c = table.concat(c, "\n", 1)
614 local entry, flags = _parse_mixed_record(c)
615 if entry then
616 entry.flags = flags
617 end
618 table.insert(iws[k], entry)
619 end
620 end
621 end
622
623 return iface and (iws[iface] or {}) or iws
624 end
625
626
627 --- LuCI system utilities / init related functions.
628 -- @class module
629 -- @name luci.sys.init
630 init = {}
631 init.dir = "/etc/init.d/"
632
633 --- Get the names of all installed init scripts
634 -- @return Table containing the names of all inistalled init scripts
635 function init.names()
636 local names = { }
637 for _, name in ipairs(luci.fs.glob(init.dir.."*")) do
638 names[#names+1] = luci.fs.basename(name)
639 end
640 return names
641 end
642
643 --- Test whether the given init script is enabled
644 -- @param name Name of the init script
645 -- @return Boolean indicating whether init is enabled
646 function init.enabled(name)
647 if luci.fs.access(init.dir..name) then
648 return ( call(init.dir..name.." enabled") == 0 )
649 end
650 return false
651 end
652
653 --- Get the index of he given init script
654 -- @param name Name of the init script
655 -- @return Numeric index value
656 function init.index(name)
657 if luci.fs.access(init.dir..name) then
658 return call("source "..init.dir..name.."; exit $START")
659 end
660 end
661
662 --- Enable the given init script
663 -- @param name Name of the init script
664 -- @return Boolean indicating success
665 function init.enable(name)
666 if luci.fs.access(init.dir..name) then
667 return ( call(init.dir..name.." enable") == 1 )
668 end
669 end
670
671 --- Disable the given init script
672 -- @param name Name of the init script
673 -- @return Boolean indicating success
674 function init.disable(name)
675 if luci.fs.access(init.dir..name) then
676 return ( call(init.dir..name.." disable") == 0 )
677 end
678 end
679
680
681 -- Internal functions
682
683 function _parse_delimited_table(iter, delimiter)
684 delimiter = delimiter or "%s+"
685
686 local data = {}
687 local trim = luci.util.trim
688 local split = luci.util.split
689
690 local keys = split(trim(iter()), delimiter, nil, true)
691 for i, j in pairs(keys) do
692 keys[i] = trim(keys[i])
693 end
694
695 for line in iter do
696 local row = {}
697 line = trim(line)
698 if #line > 0 then
699 for i, j in pairs(split(line, delimiter, nil, true)) do
700 if keys[i] then
701 row[keys[i]] = j
702 end
703 end
704 end
705 table.insert(data, row)
706 end
707
708 return data
709 end
710
711 function _parse_mixed_record(cnt, delimiter)
712 delimiter = delimiter or " "
713 local data = {}
714 local flags = {}
715
716 for i, l in pairs(luci.util.split(luci.util.trim(cnt), "\n")) do
717 for j, f in pairs(luci.util.split(luci.util.trim(l), delimiter, nil, true)) do
718 local k, x, v = f:match('([^%s][^:=]+) *([:=]*) *"*([^\n"]*)"*')
719
720 if k then
721 if x == "" then
722 table.insert(flags, k)
723 else
724 data[k] = v
725 end
726 end
727 end
728 end
729
730 return data, flags
731 end