Replace some makes-you-want-to-slash-your-wrists-Code with something less harmful
[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 else
192 model = cpuinfo:match("cpu model.-:%s*([^\n]+)")
193 end
194
195 return system, model, memtotal, memcached, membuffers, memfree
196 end
197
198 --- Retrieves the output of the "logread" command.
199 -- @return String containing the current log buffer
200 function syslog()
201 return luci.util.exec("logread")
202 end
203
204 --- Generates a random id with specified length.
205 -- @param bytes Number of bytes for the unique id
206 -- @return String containing hex encoded id
207 function uniqueid(bytes)
208 local fp = io.open("/dev/urandom")
209 local chunk = { fp:read(bytes):byte(1, bytes) }
210 fp:close()
211
212 local hex = ""
213
214 local pattern = "%02X"
215 for i, byte in ipairs(chunk) do
216 hex = hex .. pattern:format(byte)
217 end
218
219 return hex
220 end
221
222 --- Returns the current system uptime stats.
223 -- @return String containing total uptime in seconds
224 -- @return String containing idle time in seconds
225 function uptime()
226 local loadavg = io.lines("/proc/uptime")()
227 return loadavg:match("^(.-) (.-)$")
228 end
229
230 --- LuCI system utilities / POSIX user group related functions.
231 -- @class module
232 -- @name luci.sys.group
233 group = {}
234
235 --- Returns information about a POSIX user group.
236 -- @class function
237 -- @name getgroup
238 -- @param group Group ID or name of a system user group
239 -- @return Table with information about the requested group
240 group.getgroup = posix.getgroup
241
242
243 --- LuCI system utilities / network related functions.
244 -- @class module
245 -- @name luci.sys.net
246 net = {}
247
248 --- Returns the current arp-table entries as two-dimensional table.
249 -- @return Table of table containing the current arp entries.
250 -- The following fields are defined for arp entry objects:
251 -- { "IP address", "HW address", "HW type", "Flags", "Mask", "Device" }
252 function net.arptable()
253 return _parse_delimited_table(io.lines("/proc/net/arp"), "%s%s+")
254 end
255
256 --- Returns conntrack information
257 -- @return Table with the currently tracked IP connections
258 function net.conntrack()
259 local connt = {}
260 if luci.fs.access("/proc/net/nf_conntrack") then
261 for line in io.lines("/proc/net/nf_conntrack") do
262 local entry, flags = _parse_mixed_record(line, " +")
263 entry.layer3 = flags[1]
264 entry.layer4 = flags[2]
265 for i=1, #entry do
266 entry[i] = nil
267 end
268
269 connt[#connt+1] = entry
270 end
271 elseif luci.fs.access("/proc/net/ip_conntrack") then
272 for line in io.lines("/proc/net/ip_conntrack") do
273 local entry, flags = _parse_mixed_record(line, " +")
274 entry.layer3 = "ipv4"
275 entry.layer4 = flags[1]
276 for i=1, #entry do
277 entry[i] = nil
278 end
279
280 connt[#connt+1] = entry
281 end
282 else
283 return nil
284 end
285 return connt
286 end
287
288 --- Determine the current default route.
289 -- @return Table with the properties of the current default route.
290 -- The following fields are defined:
291 -- { "Mask", "RefCnt", "Iface", "Flags", "Window", "IRTT",
292 -- "MTU", "Gateway", "Destination", "Metric", "Use" }
293 function net.defaultroute()
294 local routes = net.routes()
295 local route = nil
296
297 for i, r in pairs(luci.sys.net.routes()) do
298 if r.Destination == "00000000" and (not route or route.Metric > r.Metric) then
299 route = r
300 end
301 end
302
303 return route
304 end
305
306 --- Determine the names of available network interfaces.
307 -- @return Table containing all current interface names
308 function net.devices()
309 local devices = {}
310 for line in io.lines("/proc/net/dev") do
311 table.insert(devices, line:match(" *(.-):"))
312 end
313 return devices
314 end
315
316
317 --- Return information about available network interfaces.
318 -- @return Table containing all current interface names and their information
319 function net.deviceinfo()
320 local devices = {}
321 for line in io.lines("/proc/net/dev") do
322 local name, data = line:match("^ *(.-): *(.*)$")
323 if name and data then
324 devices[name] = luci.util.split(data, " +", nil, true)
325 end
326 end
327 return devices
328 end
329
330
331 -- Determine the MAC address belonging to the given IP address.
332 -- @param ip IPv4 address
333 -- @return String containing the MAC address or nil if it cannot be found
334 function net.ip4mac(ip)
335 local mac = nil
336
337 for i, l in ipairs(net.arptable()) do
338 if l["IP address"] == ip then
339 mac = l["HW address"]
340 end
341 end
342
343 return mac
344 end
345
346 --- Returns the current kernel routing table entries.
347 -- @return Table of tables with properties of the corresponding routes.
348 -- The following fields are defined for route entry tables:
349 -- { "Mask", "RefCnt", "Iface", "Flags", "Window", "IRTT",
350 -- "MTU", "Gateway", "Destination", "Metric", "Use" }
351 function net.routes()
352 return _parse_delimited_table(io.lines("/proc/net/route"))
353 end
354
355
356 --- Tests whether the given host responds to ping probes.
357 -- @param host String containing a hostname or IPv4 address
358 -- @return Number containing 0 on success and >= 1 on error
359 function net.pingtest(host)
360 return os.execute("ping -c1 '"..host:gsub("'", '').."' >/dev/null 2>&1")
361 end
362
363
364 --- LuCI system utilities / process related functions.
365 -- @class module
366 -- @name luci.sys.process
367 process = {}
368
369 --- Get the current process id.
370 -- @class function
371 -- @name process.info
372 -- @return Number containing the current pid
373 process.info = posix.getpid
374
375 --- Retrieve information about currently running processes.
376 -- @return Table containing process information
377 function process.list()
378 local data = {}
379 local k
380 local ps = luci.util.execi("top -bn1")
381
382 if not ps then
383 return
384 end
385
386 while true do
387 local line = ps()
388 if not line then
389 return
390 end
391
392 k = luci.util.split(luci.util.trim(line), "%s+", nil, true)
393 if k[1] == "PID" then
394 break
395 end
396 end
397
398 for line in ps do
399 local row = {}
400
401 line = luci.util.trim(line)
402 for i, value in ipairs(luci.util.split(line, "%s+", #k-1, true)) do
403 row[k[i]] = value
404 end
405
406 local pid = tonumber(row[k[1]])
407 if pid then
408 data[pid] = row
409 end
410 end
411
412 return data
413 end
414
415 --- Set the gid of a process identified by given pid.
416 -- @param pid Number containing the process id
417 -- @param gid Number containing the Unix group id
418 -- @return Boolean indicating successful operation
419 -- @return String containing the error message if failed
420 -- @return Number containing the error code if failed
421 function process.setgroup(pid, gid)
422 return posix.setpid("g", pid, gid)
423 end
424
425 --- Set the uid of a process identified by given pid.
426 -- @param pid Number containing the process id
427 -- @param uid Number containing the Unix user id
428 -- @return Boolean indicating successful operation
429 -- @return String containing the error message if failed
430 -- @return Number containing the error code if failed
431 function process.setuser(pid, uid)
432 return posix.setpid("u", pid, uid)
433 end
434
435 --- Send a signal to a process identified by given pid.
436 -- @class function
437 -- @name process.signal
438 -- @param pid Number containing the process id
439 -- @param sig Signal to send (default: 15 [SIGTERM])
440 -- @return Boolean indicating successful operation
441 -- @return Number containing the error code if failed
442 process.signal = posix.kill
443
444
445 --- LuCI system utilities / user related functions.
446 -- @class module
447 -- @name luci.sys.user
448 user = {}
449
450 --- Retrieve user informations for given uid.
451 -- @class function
452 -- @name getuser
453 -- @param uid Number containing the Unix user id
454 -- @return Table containing the following fields:
455 -- { "uid", "gid", "name", "passwd", "dir", "shell", "gecos" }
456 user.getuser = posix.getpasswd
457
458 --- Test whether given string matches the password of a given system user.
459 -- @param username String containing the Unix user name
460 -- @param password String containing the password to compare
461 -- @return Boolean indicating wheather the passwords are equal
462 function user.checkpasswd(username, password)
463 local account = user.getuser(username)
464
465 if account then
466 local pwd = account.passwd
467 local shadowpw
468 if #pwd == 1 then
469 if luci.fs.stat("/etc/shadow") then
470 if not pcall(function()
471 for l in io.lines("/etc/shadow") do
472 shadowpw = l:match("^%s:([^:]+)" % username)
473 if shadowpw then
474 pwd = shadowpw
475 break
476 end
477 end
478 end) then
479 return nil, "Unable to access shadow-file"
480 end
481 end
482
483 if pwd == "!" then
484 return true
485 end
486 end
487
488 if pwd and #pwd > 0 and password and #password > 0 then
489 return (pwd == posix.crypt(password, pwd))
490 end
491 end
492
493 return false
494 end
495
496 --- Change the password of given user.
497 -- @param username String containing the Unix user name
498 -- @param password String containing the password to compare
499 -- @return Number containing 0 on success and >= 1 on error
500 function user.setpasswd(username, password)
501 if password then
502 password = password:gsub("'", "")
503 end
504
505 if username then
506 username = username:gsub("'", "")
507 end
508
509 local cmd = "(echo '"..password.."';sleep 1;echo '"..password.."')|"
510 cmd = cmd .. "passwd '"..username.."' >/dev/null 2>&1"
511 return os.execute(cmd)
512 end
513
514
515 --- LuCI system utilities / wifi related functions.
516 -- @class module
517 -- @name luci.sys.wifi
518 wifi = {}
519
520 --- Get iwconfig output for all wireless devices.
521 -- @return Table of tables containing the iwconfing output for each wifi device
522 function wifi.getiwconfig()
523 local cnt = luci.util.exec("/usr/sbin/iwconfig 2>/dev/null")
524 local iwc = {}
525
526 for i, l in pairs(luci.util.split(luci.util.trim(cnt), "\n\n")) do
527 local k = l:match("^(.-) ")
528 l = l:gsub("^(.-) +", "", 1)
529 if k then
530 local entry, flags = _parse_mixed_record(l)
531 if entry then
532 entry.flags = flags
533 end
534 iwc[k] = entry
535 end
536 end
537
538 return iwc
539 end
540
541 --- Get iwlist scan output from all wireless devices.
542 -- @return Table of tables contaiing all scan results
543 function wifi.iwscan(iface)
544 local siface = iface or ""
545 local cnt = luci.util.exec("iwlist "..siface.." scan 2>/dev/null")
546 local iws = {}
547
548 for i, l in pairs(luci.util.split(luci.util.trim(cnt), "\n\n")) do
549 local k = l:match("^(.-) ")
550 l = l:gsub("^[^\n]+", "", 1)
551 l = luci.util.trim(l)
552 if k then
553 iws[k] = {}
554 for j, c in pairs(luci.util.split(l, "\n Cell")) do
555 c = c:gsub("^(.-)- ", "", 1)
556 c = luci.util.split(c, "\n", 7)
557 c = table.concat(c, "\n", 1)
558 local entry, flags = _parse_mixed_record(c)
559 if entry then
560 entry.flags = flags
561 end
562 table.insert(iws[k], entry)
563 end
564 end
565 end
566
567 return iface and (iws[iface] or {}) or iws
568 end
569
570
571 --- LuCI system utilities / init related functions.
572 -- @class module
573 -- @name luci.sys.init
574 init = {}
575 init.dir = "/etc/init.d/"
576
577 --- Get the names of all installed init scripts
578 -- @return Table containing the names of all inistalled init scripts
579 function init.names()
580 local names = { }
581 for _, name in ipairs(luci.fs.glob(init.dir.."*")) do
582 names[#names+1] = luci.fs.basename(name)
583 end
584 return names
585 end
586
587 --- Test whether the given init script is enabled
588 -- @param name Name of the init script
589 -- @return Boolean indicating whether init is enabled
590 function init.enabled(name)
591 if luci.fs.access(init.dir..name) then
592 return ( call(init.dir..name.." enabled") == 0 )
593 end
594 return false
595 end
596
597 --- Get the index of he given init script
598 -- @param name Name of the init script
599 -- @return Numeric index value
600 function init.index(name)
601 if luci.fs.access(init.dir..name) then
602 return call("source "..init.dir..name.."; exit $START")
603 end
604 end
605
606 --- Enable the given init script
607 -- @param name Name of the init script
608 -- @return Boolean indicating success
609 function init.enable(name)
610 if luci.fs.access(init.dir..name) then
611 return ( call(init.dir..name.." enable") == 1 )
612 end
613 end
614
615 --- Disable the given init script
616 -- @param name Name of the init script
617 -- @return Boolean indicating success
618 function init.disable(name)
619 if luci.fs.access(init.dir..name) then
620 return ( call(init.dir..name.." disable") == 0 )
621 end
622 end
623
624
625 -- Internal functions
626
627 function _parse_delimited_table(iter, delimiter)
628 delimiter = delimiter or "%s+"
629
630 local data = {}
631 local trim = luci.util.trim
632 local split = luci.util.split
633
634 local keys = split(trim(iter()), delimiter, nil, true)
635 for i, j in pairs(keys) do
636 keys[i] = trim(keys[i])
637 end
638
639 for line in iter do
640 local row = {}
641 line = trim(line)
642 if #line > 0 then
643 for i, j in pairs(split(line, delimiter, nil, true)) do
644 if keys[i] then
645 row[keys[i]] = j
646 end
647 end
648 end
649 table.insert(data, row)
650 end
651
652 return data
653 end
654
655 function _parse_mixed_record(cnt, delimiter)
656 delimiter = delimiter or " "
657 local data = {}
658 local flags = {}
659
660 for i, l in pairs(luci.util.split(luci.util.trim(cnt), "\n")) do
661 for j, f in pairs(luci.util.split(luci.util.trim(l), delimiter, nil, true)) do
662 local k, x, v = f:match('([^%s][^:=]+) *([:=]*) *"*([^\n"]*)"*')
663
664 if k then
665 if x == "" then
666 table.insert(flags, k)
667 else
668 data[k] = v
669 end
670 end
671 end
672 end
673
674 return data, flags
675 end