Remove debug code from last commit
[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 = tonumber, ipairs, pairs
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 io.lines("/proc/sys/kernel/hostname")()
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.open 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 = _parse_mixed_record(line, " +")
269                         entry.layer3 = entry[1]
270                         entry.layer4 = entry[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/nf_conntrack") do
279                         local entry = _parse_mixed_record(line, " +")
280                         entry.layer3 = "ipv4"
281                         entry.layer4 = entry[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                 return (pwd == posix.crypt(password, pwd))
495         end
496 end
497
498 --- Change the password of given user.
499 -- @param username      String containing the Unix user name
500 -- @param password      String containing the password to compare
501 -- @return                      Number containing 0 on success and >= 1 on error
502 function user.setpasswd(username, password)
503         if password then
504                 password = password:gsub("'", "")
505         end
506
507         if username then
508                 username = username:gsub("'", "")
509         end
510
511         local cmd = "(echo '"..password.."';sleep 1;echo '"..password.."')|"
512         cmd = cmd .. "passwd '"..username.."' >/dev/null 2>&1"
513         return os.execute(cmd)
514 end
515
516
517 --- LuCI system utilities / wifi related functions.
518 -- @class       module
519 -- @name        luci.sys.wifi
520 wifi = {}
521
522 --- Get iwconfig output for all wireless devices.
523 -- @return      Table of tables containing the iwconfing output for each wifi device
524 function wifi.getiwconfig()
525         local cnt = luci.util.exec("/usr/sbin/iwconfig 2>/dev/null")
526         local iwc = {}
527
528         for i, l in pairs(luci.util.split(luci.util.trim(cnt), "\n\n")) do
529                 local k = l:match("^(.-) ")
530                 l = l:gsub("^(.-) +", "", 1)
531                 if k then
532                         iwc[k] = _parse_mixed_record(l)
533                 end
534         end
535
536         return iwc
537 end
538
539 --- Get iwlist scan output from all wireless devices.
540 -- @return      Table of tables contaiing all scan results
541 function wifi.iwscan(iface)
542         local siface = iface or ""
543         local cnt = luci.util.exec("iwlist "..siface.." scan 2>/dev/null")
544         local iws = {}
545
546         for i, l in pairs(luci.util.split(luci.util.trim(cnt), "\n\n")) do
547                 local k = l:match("^(.-) ")
548                 l = l:gsub("^[^\n]+", "", 1)
549                 l = luci.util.trim(l)
550                 if k then
551                         iws[k] = {}
552                         for j, c in pairs(luci.util.split(l, "\n          Cell")) do
553                                 c = c:gsub("^(.-)- ", "", 1)
554                                 c = luci.util.split(c, "\n", 7)
555                                 c = table.concat(c, "\n", 1)
556                                 table.insert(iws[k], _parse_mixed_record(c))
557                         end
558                 end
559         end
560
561         return iface and (iws[iface] or {}) or iws
562 end
563
564
565 --- LuCI system utilities / init related functions.
566 -- @class       module
567 -- @name        luci.sys.init
568 init = {}
569 init.dir = "/etc/init.d/"
570
571 --- Get the names of all installed init scripts
572 -- @return      Table containing the names of all inistalled init scripts
573 function init.names()
574         local names = { }
575         for _, name in ipairs(luci.fs.glob(init.dir.."*")) do
576                 names[#names+1] = luci.fs.basename(name)
577         end
578         return names
579 end
580
581 --- Test whether the given init script is enabled
582 -- @param name  Name of the init script
583 -- @return              Boolean indicating whether init is enabled
584 function init.enabled(name)
585         if luci.fs.access(init.dir..name) then
586                 return ( call(init.dir..name.." enabled") == 0 )
587         end
588         return false
589 end
590
591 --- Get the index of he given init script
592 -- @param name  Name of the init script
593 -- @return              Numeric index value
594 function init.index(name)
595         if luci.fs.access(init.dir..name) then
596                 return call("source "..init.dir..name.."; exit $START")
597         end
598 end
599
600 --- Enable the given init script
601 -- @param name  Name of the init script
602 -- @return              Boolean indicating success
603 function init.enable(name)
604         if luci.fs.access(init.dir..name) then
605                 return ( call(init.dir..name.." enable") == 1 )
606         end
607 end
608
609 --- Disable the given init script
610 -- @param name  Name of the init script
611 -- @return              Boolean indicating success
612 function init.disable(name)
613         if luci.fs.access(init.dir..name) then
614                 return ( call(init.dir..name.." disable") == 0 )
615         end
616 end
617
618
619 -- Internal functions
620
621 function _parse_delimited_table(iter, delimiter)
622         delimiter = delimiter or "%s+"
623
624         local data  = {}
625         local trim  = luci.util.trim
626         local split = luci.util.split
627
628         local keys = split(trim(iter()), delimiter, nil, true)
629         for i, j in pairs(keys) do
630                 keys[i] = trim(keys[i])
631         end
632
633         for line in iter do
634                 local row = {}
635                 line = trim(line)
636                 if #line > 0 then
637                         for i, j in pairs(split(line, delimiter, nil, true)) do
638                                 if keys[i] then
639                                         row[keys[i]] = j
640                                 end
641                         end
642                 end
643                 table.insert(data, row)
644         end
645
646         return data
647 end
648
649 function _parse_mixed_record(cnt, delimiter)
650         delimiter = delimiter or "  "
651         local data = {}
652
653         for i, l in pairs(luci.util.split(luci.util.trim(cnt), "\n")) do
654                 for j, f in pairs(luci.util.split(luci.util.trim(l), delimiter, nil, true)) do
655                 local k, x, v = f:match('([^%s][^:=]+) *([:=]*) *"*([^\n"]*)"*')
656
657             if k then
658                                 if x == "" then
659                                         table.insert(data, k)
660                                 else
661                         data[k] = v
662                                 end
663             end
664         end
665         end
666
667     return data
668 end