c271dee86b8a4ad5bbeb154d8f7abfc3eb8ccfb3
[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 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/ip_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                 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                         iwc[k] = _parse_mixed_record(l)
537                 end
538         end
539
540         return iwc
541 end
542
543 --- Get iwlist scan output from all wireless devices.
544 -- @return      Table of tables contaiing all scan results
545 function wifi.iwscan(iface)
546         local siface = iface or ""
547         local cnt = luci.util.exec("iwlist "..siface.." scan 2>/dev/null")
548         local iws = {}
549
550         for i, l in pairs(luci.util.split(luci.util.trim(cnt), "\n\n")) do
551                 local k = l:match("^(.-) ")
552                 l = l:gsub("^[^\n]+", "", 1)
553                 l = luci.util.trim(l)
554                 if k then
555                         iws[k] = {}
556                         for j, c in pairs(luci.util.split(l, "\n          Cell")) do
557                                 c = c:gsub("^(.-)- ", "", 1)
558                                 c = luci.util.split(c, "\n", 7)
559                                 c = table.concat(c, "\n", 1)
560                                 table.insert(iws[k], _parse_mixed_record(c))
561                         end
562                 end
563         end
564
565         return iface and (iws[iface] or {}) or iws
566 end
567
568
569 --- LuCI system utilities / init related functions.
570 -- @class       module
571 -- @name        luci.sys.init
572 init = {}
573 init.dir = "/etc/init.d/"
574
575 --- Get the names of all installed init scripts
576 -- @return      Table containing the names of all inistalled init scripts
577 function init.names()
578         local names = { }
579         for _, name in ipairs(luci.fs.glob(init.dir.."*")) do
580                 names[#names+1] = luci.fs.basename(name)
581         end
582         return names
583 end
584
585 --- Test whether the given init script is enabled
586 -- @param name  Name of the init script
587 -- @return              Boolean indicating whether init is enabled
588 function init.enabled(name)
589         if luci.fs.access(init.dir..name) then
590                 return ( call(init.dir..name.." enabled") == 0 )
591         end
592         return false
593 end
594
595 --- Get the index of he given init script
596 -- @param name  Name of the init script
597 -- @return              Numeric index value
598 function init.index(name)
599         if luci.fs.access(init.dir..name) then
600                 return call("source "..init.dir..name.."; exit $START")
601         end
602 end
603
604 --- Enable the given init script
605 -- @param name  Name of the init script
606 -- @return              Boolean indicating success
607 function init.enable(name)
608         if luci.fs.access(init.dir..name) then
609                 return ( call(init.dir..name.." enable") == 1 )
610         end
611 end
612
613 --- Disable the given init script
614 -- @param name  Name of the init script
615 -- @return              Boolean indicating success
616 function init.disable(name)
617         if luci.fs.access(init.dir..name) then
618                 return ( call(init.dir..name.." disable") == 0 )
619         end
620 end
621
622
623 -- Internal functions
624
625 function _parse_delimited_table(iter, delimiter)
626         delimiter = delimiter or "%s+"
627
628         local data  = {}
629         local trim  = luci.util.trim
630         local split = luci.util.split
631
632         local keys = split(trim(iter()), delimiter, nil, true)
633         for i, j in pairs(keys) do
634                 keys[i] = trim(keys[i])
635         end
636
637         for line in iter do
638                 local row = {}
639                 line = trim(line)
640                 if #line > 0 then
641                         for i, j in pairs(split(line, delimiter, nil, true)) do
642                                 if keys[i] then
643                                         row[keys[i]] = j
644                                 end
645                         end
646                 end
647                 table.insert(data, row)
648         end
649
650         return data
651 end
652
653 function _parse_mixed_record(cnt, delimiter)
654         delimiter = delimiter or "  "
655         local data = {}
656
657         for i, l in pairs(luci.util.split(luci.util.trim(cnt), "\n")) do
658                 for j, f in pairs(luci.util.split(luci.util.trim(l), delimiter, nil, true)) do
659                 local k, x, v = f:match('([^%s][^:=]+) *([:=]*) *"*([^\n"]*)"*')
660
661             if k then
662                                 if x == "" then
663                                         table.insert(data, k)
664                                 else
665                         data[k] = v
666                                 end
667             end
668         end
669         end
670
671     return data
672 end