* luci/themes: fix log pages
[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
365 --- Tests whether the given host responds to ping probes.
366 -- @param host  String containing a hostname or IPv4 address
367 -- @return              Number containing 0 on success and >= 1 on error
368 function net.pingtest(host)
369         return os.execute("ping -c1 '"..host:gsub("'", '').."' >/dev/null 2>&1")
370 end
371
372
373 --- LuCI system utilities / process related functions.
374 -- @class       module
375 -- @name        luci.sys.process
376 process = {}
377
378 --- Get the current process id.
379 -- @class function
380 -- @name  process.info
381 -- @return      Number containing the current pid
382 process.info = posix.getpid
383
384 --- Retrieve information about currently running processes.
385 -- @return      Table containing process information
386 function process.list()
387         local data = {}
388         local k
389         local ps = luci.util.execi("top -bn1")
390
391         if not ps then
392                 return
393         end
394
395         while true do
396                 local line = ps()
397                 if not line then
398                         return
399                 end
400
401                 k = luci.util.split(luci.util.trim(line), "%s+", nil, true)
402                 if k[1] == "PID" then
403                         break
404                 end
405         end
406
407         for line in ps do
408                 local row = {}
409
410                 line = luci.util.trim(line)
411                 for i, value in ipairs(luci.util.split(line, "%s+", #k-1, true)) do
412                         row[k[i]] = value
413                 end
414
415                 local pid = tonumber(row[k[1]])
416                 if pid then
417                         data[pid] = row
418                 end
419         end
420
421         return data
422 end
423
424 --- Set the gid of a process identified by given pid.
425 -- @param pid   Number containing the process id
426 -- @param gid   Number containing the Unix group id
427 -- @return              Boolean indicating successful operation
428 -- @return              String containing the error message if failed
429 -- @return              Number containing the error code if failed
430 function process.setgroup(pid, gid)
431         return posix.setpid("g", pid, gid)
432 end
433
434 --- Set the uid of a process identified by given pid.
435 -- @param pid   Number containing the process id
436 -- @param uid   Number containing the Unix user id
437 -- @return              Boolean indicating successful operation
438 -- @return              String containing the error message if failed
439 -- @return              Number containing the error code if failed
440 function process.setuser(pid, uid)
441         return posix.setpid("u", pid, uid)
442 end
443
444 --- Send a signal to a process identified by given pid.
445 -- @class function
446 -- @name  process.signal
447 -- @param pid   Number containing the process id
448 -- @param sig   Signal to send (default: 15 [SIGTERM])
449 -- @return              Boolean indicating successful operation
450 -- @return              Number containing the error code if failed
451 process.signal = posix.kill
452
453
454 --- LuCI system utilities / user related functions.
455 -- @class       module
456 -- @name        luci.sys.user
457 user = {}
458
459 --- Retrieve user informations for given uid.
460 -- @class               function
461 -- @name                getuser
462 -- @param uid   Number containing the Unix user id
463 -- @return              Table containing the following fields:
464 --                              { "uid", "gid", "name", "passwd", "dir", "shell", "gecos" }
465 user.getuser = posix.getpasswd
466
467 --- Test whether given string matches the password of a given system user.
468 -- @param username      String containing the Unix user name
469 -- @param password      String containing the password to compare
470 -- @return                      Boolean indicating wheather the passwords are equal
471 function user.checkpasswd(username, password)
472         local account = user.getuser(username)
473
474         if account then
475                 local pwd = account.passwd
476                 local shadowpw
477                 if #pwd == 1 then
478                         if luci.fs.stat("/etc/shadow") then
479                                 if not pcall(function()
480                                         for l in io.lines("/etc/shadow") do
481                                                 shadowpw = l:match("^%s:([^:]+)" % username)
482                                                 if shadowpw then
483                                                         pwd = shadowpw
484                                                         break
485                                                 end
486                                         end
487                                 end) then
488                                         return nil, "Unable to access shadow-file"
489                                 end
490                         end
491
492                         if pwd == "!" then
493                                 return true
494                         end
495                 end
496
497                 if pwd and #pwd > 0 and password and #password > 0 then
498                         return (pwd == posix.crypt(password, pwd))
499                 end
500         end
501
502         return false
503 end
504
505 --- Change the password of given user.
506 -- @param username      String containing the Unix user name
507 -- @param password      String containing the password to compare
508 -- @return                      Number containing 0 on success and >= 1 on error
509 function user.setpasswd(username, password)
510         if password then
511                 password = password:gsub("'", "")
512         end
513
514         if username then
515                 username = username:gsub("'", "")
516         end
517
518         local cmd = "(echo '"..password.."';sleep 1;echo '"..password.."')|"
519         cmd = cmd .. "passwd '"..username.."' >/dev/null 2>&1"
520         return os.execute(cmd)
521 end
522
523
524 --- LuCI system utilities / wifi related functions.
525 -- @class       module
526 -- @name        luci.sys.wifi
527 wifi = {}
528
529 --- Get iwconfig output for all wireless devices.
530 -- @return      Table of tables containing the iwconfing output for each wifi device
531 function wifi.getiwconfig()
532         local cnt = luci.util.exec("/usr/sbin/iwconfig 2>/dev/null")
533         local iwc = {}
534
535         for i, l in pairs(luci.util.split(luci.util.trim(cnt), "\n\n")) do
536                 local k = l:match("^(.-) ")
537                 l = l:gsub("^(.-) +", "", 1)
538                 if k then
539                         local entry, flags = _parse_mixed_record(l)
540                         if entry then
541                                 entry.flags = flags
542                         end
543                         iwc[k] = entry
544                 end
545         end
546
547         return iwc
548 end
549
550 --- Get iwlist scan output from all wireless devices.
551 -- @return      Table of tables contaiing all scan results
552 function wifi.iwscan(iface)
553         local siface = iface or ""
554         local cnt = luci.util.exec("iwlist "..siface.." scan 2>/dev/null")
555         local iws = {}
556
557         for i, l in pairs(luci.util.split(luci.util.trim(cnt), "\n\n")) do
558                 local k = l:match("^(.-) ")
559                 l = l:gsub("^[^\n]+", "", 1)
560                 l = luci.util.trim(l)
561                 if k then
562                         iws[k] = {}
563                         for j, c in pairs(luci.util.split(l, "\n          Cell")) do
564                                 c = c:gsub("^(.-)- ", "", 1)
565                                 c = luci.util.split(c, "\n", 7)
566                                 c = table.concat(c, "\n", 1)
567                                 local entry, flags = _parse_mixed_record(c)
568                                 if entry then
569                                         entry.flags = flags
570                                 end
571                                 table.insert(iws[k], entry)
572                         end
573                 end
574         end
575
576         return iface and (iws[iface] or {}) or iws
577 end
578
579
580 --- LuCI system utilities / init related functions.
581 -- @class       module
582 -- @name        luci.sys.init
583 init = {}
584 init.dir = "/etc/init.d/"
585
586 --- Get the names of all installed init scripts
587 -- @return      Table containing the names of all inistalled init scripts
588 function init.names()
589         local names = { }
590         for _, name in ipairs(luci.fs.glob(init.dir.."*")) do
591                 names[#names+1] = luci.fs.basename(name)
592         end
593         return names
594 end
595
596 --- Test whether the given init script is enabled
597 -- @param name  Name of the init script
598 -- @return              Boolean indicating whether init is enabled
599 function init.enabled(name)
600         if luci.fs.access(init.dir..name) then
601                 return ( call(init.dir..name.." enabled") == 0 )
602         end
603         return false
604 end
605
606 --- Get the index of he given init script
607 -- @param name  Name of the init script
608 -- @return              Numeric index value
609 function init.index(name)
610         if luci.fs.access(init.dir..name) then
611                 return call("source "..init.dir..name.."; exit $START")
612         end
613 end
614
615 --- Enable the given init script
616 -- @param name  Name of the init script
617 -- @return              Boolean indicating success
618 function init.enable(name)
619         if luci.fs.access(init.dir..name) then
620                 return ( call(init.dir..name.." enable") == 1 )
621         end
622 end
623
624 --- Disable the given init script
625 -- @param name  Name of the init script
626 -- @return              Boolean indicating success
627 function init.disable(name)
628         if luci.fs.access(init.dir..name) then
629                 return ( call(init.dir..name.." disable") == 0 )
630         end
631 end
632
633
634 -- Internal functions
635
636 function _parse_delimited_table(iter, delimiter)
637         delimiter = delimiter or "%s+"
638
639         local data  = {}
640         local trim  = luci.util.trim
641         local split = luci.util.split
642
643         local keys = split(trim(iter()), delimiter, nil, true)
644         for i, j in pairs(keys) do
645                 keys[i] = trim(keys[i])
646         end
647
648         for line in iter do
649                 local row = {}
650                 line = trim(line)
651                 if #line > 0 then
652                         for i, j in pairs(split(line, delimiter, nil, true)) do
653                                 if keys[i] then
654                                         row[keys[i]] = j
655                                 end
656                         end
657                 end
658                 table.insert(data, row)
659         end
660
661         return data
662 end
663
664 function _parse_mixed_record(cnt, delimiter)
665         delimiter = delimiter or "  "
666         local data = {}
667         local flags = {}
668
669         for i, l in pairs(luci.util.split(luci.util.trim(cnt), "\n")) do
670                 for j, f in pairs(luci.util.split(luci.util.trim(l), delimiter, nil, true)) do
671                 local k, x, v = f:match('([^%s][^:=]+) *([:=]*) *"*([^\n"]*)"*')
672
673             if k then
674                                 if x == "" then
675                                         table.insert(flags, k)
676                                 else
677                         data[k] = v
678                                 end
679             end
680         end
681         end
682
683     return data, flags
684 end