217a36b0f4a1ef6db8767701982cc2708fb9fe3f
[project/luci.git] / libs / core / 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 --- LuCI system utilities.
28 module("luci.sys", package.seeall)
29 require("posix")
30 require("luci.bits")
31 require("luci.util")
32 require("luci.fs")
33
34 --- Test wheather the current system is operating in big endian mode.
35 -- @return      Boolean value indicating wheather system is big endian
36 function bigendian()
37         local fp = io.open("/bin/sh")
38         fp:seek("set", 5)
39         local be = (fp:read(1):byte() ~= 1)
40         fp:close()
41         return be
42 end
43
44 --- Execute given commandline and gather stdout.
45 -- @param command       String containing command to execute
46 -- @return                      String containing the command's stdout
47 function exec(command)
48         local pp   = io.popen(command)
49         local data = pp:read("*a")
50         pp:close()
51
52         return data
53 end
54
55 --- Execute given commandline and gather stdout.
56 -- @param command       String containing the command to execute
57 -- @return                      Table containing the command's stdout splitted up in lines
58 function execl(command)
59         local pp   = io.popen(command)
60         local line = ""
61         local data = {}
62
63         while true do
64                 line = pp:read()
65                 if (line == nil) then break end
66                 table.insert(data, line)
67         end
68         pp:close()
69
70         return data
71 end
72
73 --- Invoke the luci-flash executable to write an image to the flash memory.
74 -- @param kpattern      Pattern of files to keep over flash process
75 -- @return                      Return value of os.execute()
76 function flash(image, kpattern)
77         local cmd = "luci-flash "
78         if kpattern then
79                 cmd = cmd .. "-k '" .. kpattern:gsub("'", "") .. "' "
80         end
81         cmd = cmd .. "'" .. image:gsub("'", "") .. "' >/dev/null 2>&1"
82
83         return os.execute(cmd)
84 end
85
86 --- Retrieve environment variables. If no variable is given then a table
87 -- containing the whole environment is returned otherwise this function returns
88 -- the corresponding string value for the given name or nil if no such variable
89 -- exists.
90 -- @class               function
91 -- @name                getenv
92 -- @param var   Name of the environment variable to retrieve (optional)
93 -- @return              String containg the value of the specified variable
94 -- @return              Table containing all variables if no variable name is given
95 getenv = posix.getenv
96
97 --- Determine the current hostname.
98 -- @return              String containing the system hostname
99 function hostname()
100         return io.lines("/proc/sys/kernel/hostname")()
101 end
102
103 --- Returns the contents of a documented referred by an URL.
104 -- @param url   The URL to retrieve
105 -- @return              String containing the contents of given the URL
106 function httpget(url)
107         return exec("wget -qO- '"..url:gsub("'", "").."'")
108 end
109
110 --- Returns the absolute path to LuCI base directory.
111 -- @return              String containing the directory path
112 function libpath()
113         return luci.fs.dirname(require("luci.debug").__file__)
114 end
115
116 --- Returns the system load average values.
117 -- @return      String containing the average load value 1 minute ago
118 -- @return      String containing the average load value 5 minutes ago
119 -- @return      String containing the average load value 15 minutes ago
120 -- @return      String containing the active and total number of processes
121 -- @return      String containing the last used pid
122 function loadavg()
123         local loadavg = io.lines("/proc/loadavg")()
124         return loadavg:match("^(.-) (.-) (.-) (.-) (.-)$")
125 end
126
127 --- Initiate a system reboot.
128 -- @return      Return value of os.execute()
129 function reboot()
130         return os.execute("reboot >/dev/null 2>&1")
131 end
132
133 --- Returns the system type, cpu name and installed physical memory.
134 -- @return      String containing the system or platform identifier
135 -- @return      String containing hardware model information
136 -- @return      String containing the total memory amount in kB
137 -- @return      String containing the memory used for caching in kB
138 -- @return      String containing the memory used for buffering in kB
139 -- @return      String containing the free memory amount in kB
140 -- @return      Number containing free memory in percent
141 -- @return      Number containing buffer memory in percent
142 -- @return      Number containing cache memory in percent
143 function sysinfo()
144         local c1 = "cat /proc/cpuinfo|grep system\\ typ|cut -d: -f2 2>/dev/null"
145         local c2 = "uname -m 2>/dev/null"
146         local c3 = "cat /proc/cpuinfo|grep model\\ name|cut -d: -f2 2>/dev/null"
147         local c4 = "cat /proc/cpuinfo|grep cpu\\ model|cut -d: -f2 2>/dev/null"
148         local c5 = "cat /proc/meminfo|grep MemTotal|awk {' print $2 '} 2>/dev/null"
149         local c6 = "cat /proc/meminfo|grep ^Cached|awk {' print $2 '} 2>/dev/null"
150         local c7 = "cat /proc/meminfo|grep MemFree|awk {' print $2 '} 2>/dev/null"
151         local c8 = "cat /proc/meminfo|grep Buffers|awk {' print $2 '} 2>/dev/null"
152
153         local system = luci.util.trim(exec(c1))
154         local model = ""
155         local memtotal = luci.util.trim(exec(c5))
156         local memcached = luci.util.trim(exec(c6))
157         local memfree = luci.util.trim(exec(c7))
158         local membuffers = luci.util.trim(exec(c8))
159         local perc_memfree = math.floor((memfree/memtotal)*100)
160         local perc_membuffers = math.floor((membuffers/memtotal)*100)
161         local perc_memcached = math.floor((memcached/memtotal)*100)
162
163         if system == "" then
164                 system = luci.util.trim(exec(c2))
165                 model = luci.util.trim(exec(c3))
166         else
167                 model = luci.util.trim(exec(c4))
168         end
169
170         return system, model, memtotal, memcached, membuffers, memfree, perc_memfree, perc_membuffers, perc_memcached
171 end
172
173 --- Retrieves the output of the "logread" command.
174 -- @return      String containing the current log buffer
175 function syslog()
176         return exec("logread")
177 end
178
179 --- Generates a random id with specified length.
180 -- @param bytes Number of bytes for the unique id
181 -- @return              String containing hex encoded id
182 function uniqueid(bytes)
183         local fp    = io.open("/dev/urandom")
184         local chunk = { fp:read(bytes):byte(1, bytes) }
185         fp:close()
186
187         local hex = ""
188
189         local pattern = "%02X"
190         for i, byte in ipairs(chunk) do
191                 hex = hex .. pattern:format(byte)
192         end
193
194         return hex
195 end
196
197 --- Returns the current system uptime stats.
198 -- @return      String containing total uptime in seconds
199 -- @return      String containing idle time in seconds
200 function uptime()
201         local loadavg = io.lines("/proc/uptime")()
202         return loadavg:match("^(.-) (.-)$")
203 end
204
205 --- LuCI system utilities / POSIX user group related functions.
206 -- @class       module
207 -- @name        luci.sys.group
208 group = {}
209
210 --- Returns information about a POSIX user group.
211 -- @param group Group ID or name of a system user group
212 -- @return      Table with information about the requested group
213 group.getgroup = posix.getgroup
214
215
216 --- LuCI system utilities / network related functions.
217 -- @class       module
218 -- @name        luci.sys.net
219 net = {}
220
221 --- Returns the current arp-table entries as two-dimensional table.
222 -- @return      Table of table containing the current arp entries.
223 --                      The following fields are defined for arp entry objects:
224 --                      { "IP address", "HW address", "HW type", "Flags", "Mask", "Device" }
225 function net.arptable()
226         return _parse_delimited_table(io.lines("/proc/net/arp"), "%s%s+")
227 end
228
229 --- Test whether an IP-Adress belongs to a certain net.
230 -- @param ip            IPv4 address to test
231 -- @param ipnet         IPv4 network address of the net range to compare against
232 -- @param prefix        Network prefix of the net range to compare against
233 -- @return                      Boolean indicating wheather the ip is within the range
234 function net.belongs(ip, ipnet, prefix)
235         return (net.ip4bin(ip):sub(1, prefix) == net.ip4bin(ipnet):sub(1, prefix))
236 end
237
238 --- Determine the current default route.
239 -- @return      Table with the properties of the current default route.
240 --                      The following fields are defined:
241 --                      { "Mask", "RefCnt", "Iface", "Flags", "Window", "IRTT",
242 --                        "MTU", "Gateway", "Destination", "Metric", "Use" }
243 function net.defaultroute()
244         local routes = net.routes()
245         local route = nil
246
247         for i, r in pairs(luci.sys.net.routes()) do
248                 if r.Destination == "00000000" and (not route or route.Metric > r.Metric) then
249                         route = r
250                 end
251         end
252
253         return route
254 end
255
256 --- Determine the names of available network interfaces.
257 -- @return      Table containing all current interface names
258 function net.devices()
259         local devices = {}
260         for line in io.lines("/proc/net/dev") do
261                 table.insert(devices, line:match(" *(.-):"))
262         end
263         return devices
264 end
265
266 -- Determine the MAC address belonging to the given IP address.
267 -- @param ip    IPv4 address
268 -- @return              String containing the MAC address or nil if it cannot be found
269 function net.ip4mac(ip)
270         local mac = nil
271
272         for i, l in ipairs(net.arptable()) do
273                 if l["IP address"] == ip then
274                         mac = l["HW address"]
275                 end
276         end
277
278         return mac
279 end
280
281 --- Calculate the prefix from a given netmask.
282 -- @param mask  IPv4 net mask
283 -- @return              Number containing the corresponding numerical prefix
284 function net.mask4prefix(mask)
285         local bin = net.ip4bin(mask)
286
287         if not bin then
288                 return nil
289         end
290
291         return #luci.util.split(bin, "1")-1
292 end
293
294 --- Returns the current kernel routing table entries.
295 -- @return      Table of tables with properties of the corresponding routes.
296 --                      The following fields are defined for route entry tables:
297 --                      { "Mask", "RefCnt", "Iface", "Flags", "Window", "IRTT",
298 --                        "MTU", "Gateway", "Destination", "Metric", "Use" }
299 function net.routes()
300         return _parse_delimited_table(io.lines("/proc/net/route"))
301 end
302
303 --- Convert hexadecimal 32 bit value to IPv4 address.
304 -- @param hex   String containing the hexadecimal value
305 -- @param be    Boolean indicating wheather the given value is big endian
306 -- @return              String containing the corresponding IP4 address
307 function net.hexip4(hex, be)
308         if #hex ~= 8 then
309                 return nil
310         end
311
312         be = be or bigendian()
313
314         local hexdec = luci.bits.Hex2Dec
315
316         local ip = ""
317         if be then
318                 ip = ip .. tostring(hexdec(hex:sub(1,2))) .. "."
319                 ip = ip .. tostring(hexdec(hex:sub(3,4))) .. "."
320                 ip = ip .. tostring(hexdec(hex:sub(5,6))) .. "."
321                 ip = ip .. tostring(hexdec(hex:sub(7,8)))
322         else
323                 ip = ip .. tostring(hexdec(hex:sub(7,8))) .. "."
324                 ip = ip .. tostring(hexdec(hex:sub(5,6))) .. "."
325                 ip = ip .. tostring(hexdec(hex:sub(3,4))) .. "."
326                 ip = ip .. tostring(hexdec(hex:sub(1,2)))
327         end
328
329         return ip
330 end
331
332 --- Convert given IPv4 address to binary value.
333 -- @param ip    String containing a IPv4 address
334 -- @return              String containing corresponding binary value
335 function net.ip4bin(ip)
336         local parts = luci.util.split(ip, '.')
337         if #parts ~= 4 then
338                 return nil
339         end
340
341         local decbin = luci.bits.Dec2Bin
342
343         local bin = ""
344         bin = bin .. decbin(parts[1], 8)
345         bin = bin .. decbin(parts[2], 8)
346         bin = bin .. decbin(parts[3], 8)
347         bin = bin .. decbin(parts[4], 8)
348
349         return bin
350 end
351
352 --- Tests whether the given host responds to ping probes.
353 -- @param host  String containing a hostname or IPv4 address
354 -- @return              Number containing 0 on success and >= 1 on error
355 function net.pingtest(host)
356         return os.execute("ping -c1 '"..host:gsub("'", '').."' >/dev/null 2>&1")
357 end
358
359
360 --- LuCI system utilities / process related functions.
361 -- @class       module
362 -- @name        luci.sys.process
363 process = {}
364
365 --- Get the current process id.
366 -- @return      Number containing the current pid
367 process.info = posix.getpid
368
369 --- Set the gid of a process identified by given pid.
370 -- @param pid   Number containing the process id
371 -- @param gid   Number containing the Unix group id
372 -- @return              Boolean indicating successful operation
373 -- @return              String containing the error message if failed
374 -- @return              Number containing the error code if failed
375 function process.setgroup(pid, gid)
376         return posix.setpid("g", pid, gid)
377 end
378
379 --- Set the uid of a process identified by given pid.
380 -- @param pid   Number containing the process id
381 -- @param uid   Number containing the Unix user id
382 -- @return              Boolean indicating successful operation
383 -- @return              String containing the error message if failed
384 -- @return              Number containing the error code if failed
385 function process.setuser(pid, uid)
386         return posix.setpid("u", pid, uid)
387 end
388
389
390 --- LuCI system utilities / user related functions.
391 -- @class       module
392 -- @name        luci.sys.user
393 user = {}
394
395 --- Retrieve user informations for given uid.
396 -- @class               function
397 -- @name                getuser
398 -- @param uid   Number containing the Unix user id
399 -- @return              Table containing the following fields:
400 --                              { "uid", "gid", "name", "passwd", "dir", "shell", "gecos" }
401 user.getuser = posix.getpasswd
402
403 --- Test whether given string matches the password of a given system user.
404 -- @param username      String containing the Unix user name
405 -- @param password      String containing the password to compare
406 -- @return                      Boolean indicating wheather the passwords are equal
407 function user.checkpasswd(username, password)
408         local account = user.getuser(username)
409
410         -- FIXME: detect testing environment
411         if luci.fs.stat("/etc/shadow") and not luci.fs.access("/etc/shadow", "r") then
412                 return true
413         elseif account then
414                 if account.passwd == "!" then
415                         return true
416                 else
417                         return (account.passwd == posix.crypt(password, account.passwd))
418                 end
419         end
420 end
421
422 --- Change the password of given user.
423 -- @param username      String containing the Unix user name
424 -- @param password      String containing the password to compare
425 -- @return                      Number containing 0 on success and >= 1 on error
426 function user.setpasswd(username, password)
427         if password then
428                 password = password:gsub("'", "")
429         end
430
431         if username then
432                 username = username:gsub("'", "")
433         end
434
435         local cmd = "(echo '"..password.."';sleep 1;echo '"..password.."')|"
436         cmd = cmd .. "passwd '"..username.."' >/dev/null 2>&1"
437         return os.execute(cmd)
438 end
439
440
441 --- LuCI system utilities / wifi related functions.
442 -- @class       module
443 -- @name        luci.sys.wifi
444 wifi = {}
445
446 --- Get iwconfig output for all wireless devices.
447 -- @return      Table of tables containing the iwconfing output for each wifi device
448 function wifi.getiwconfig()
449         local cnt = exec("/usr/sbin/iwconfig 2>/dev/null")
450         local iwc = {}
451
452         for i, l in pairs(luci.util.split(luci.util.trim(cnt), "\n\n")) do
453                 local k = l:match("^(.-) ")
454                 l = l:gsub("^(.-) +", "", 1)
455                 if k then
456                         iwc[k] = _parse_mixed_record(l)
457                 end
458         end
459
460         return iwc
461 end
462
463 --- Get iwlist scan output from all wireless devices.
464 -- @return      Table of tables contaiing all scan results
465 function wifi.iwscan()
466         local cnt = exec("iwlist scan 2>/dev/null")
467         local iws = {}
468
469         for i, l in pairs(luci.util.split(luci.util.trim(cnt), "\n\n")) do
470                 local k = l:match("^(.-) ")
471                 l = l:gsub("^[^\n]+", "", 1)
472                 l = luci.util.trim(l)
473                 if k then
474                         iws[k] = {}
475                         for j, c in pairs(luci.util.split(l, "\n          Cell")) do
476                                 c = c:gsub("^(.-)- ", "", 1)
477                                 c = luci.util.split(c, "\n", 7)
478                                 c = table.concat(c, "\n", 1)
479                                 table.insert(iws[k], _parse_mixed_record(c))
480                         end
481                 end
482         end
483
484         return iws
485 end
486
487
488 -- Internal functions
489
490 function _parse_delimited_table(iter, delimiter)
491         delimiter = delimiter or "%s+"
492
493         local data  = {}
494         local trim  = luci.util.trim
495         local split = luci.util.split
496
497         local keys = split(trim(iter()), delimiter, nil, true)
498         for i, j in pairs(keys) do
499                 keys[i] = trim(keys[i])
500         end
501
502         for line in iter do
503                 local row = {}
504                 line = trim(line)
505                 if #line > 0 then
506                         for i, j in pairs(split(line, delimiter, nil, true)) do
507                                 if keys[i] then
508                                         row[keys[i]] = j
509                                 end
510                         end
511                 end
512                 table.insert(data, row)
513         end
514
515         return data
516 end
517
518 function _parse_mixed_record(cnt)
519         local data = {}
520
521         for i, l in pairs(luci.util.split(luci.util.trim(cnt), "\n")) do
522         for j, f in pairs(luci.util.split(luci.util.trim(l), "  ")) do
523                 local k, x, v = f:match('([^%s][^:=]+) *([:=]*) *"*([^\n"]*)"*')
524
525             if k then
526                                 if x == "" then
527                                         table.insert(data, k)
528                                 else
529                         data[k] = v
530                                 end
531             end
532         end
533         end
534
535     return data
536 end