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