* luci/lib/sys: fixes for sys.init api
[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 --- Determine the current default route.
263 -- @return      Table with the properties of the current default route.
264 --                      The following fields are defined:
265 --                      { "Mask", "RefCnt", "Iface", "Flags", "Window", "IRTT",
266 --                        "MTU", "Gateway", "Destination", "Metric", "Use" }
267 function net.defaultroute()
268         local routes = net.routes()
269         local route = nil
270
271         for i, r in pairs(luci.sys.net.routes()) do
272                 if r.Destination == "00000000" and (not route or route.Metric > r.Metric) then
273                         route = r
274                 end
275         end
276
277         return route
278 end
279
280 --- Determine the names of available network interfaces.
281 -- @return      Table containing all current interface names
282 function net.devices()
283         local devices = {}
284         for line in io.lines("/proc/net/dev") do
285                 table.insert(devices, line:match(" *(.-):"))
286         end
287         return devices
288 end
289
290
291 --- Return information about available network interfaces.
292 -- @return      Table containing all current interface names and their information
293 function net.deviceinfo()
294         local devices = {}
295         for line in io.lines("/proc/net/dev") do
296                 local name, data = line:match("^ *(.-): *(.*)$")
297                 if name and data then
298                         devices[name] = luci.util.split(data, " +", nil, true)
299                 end
300         end
301         return devices
302 end
303
304
305 -- Determine the MAC address belonging to the given IP address.
306 -- @param ip    IPv4 address
307 -- @return              String containing the MAC address or nil if it cannot be found
308 function net.ip4mac(ip)
309         local mac = nil
310
311         for i, l in ipairs(net.arptable()) do
312                 if l["IP address"] == ip then
313                         mac = l["HW address"]
314                 end
315         end
316
317         return mac
318 end
319
320 --- Returns the current kernel routing table entries.
321 -- @return      Table of tables with properties of the corresponding routes.
322 --                      The following fields are defined for route entry tables:
323 --                      { "Mask", "RefCnt", "Iface", "Flags", "Window", "IRTT",
324 --                        "MTU", "Gateway", "Destination", "Metric", "Use" }
325 function net.routes()
326         return _parse_delimited_table(io.lines("/proc/net/route"))
327 end
328
329
330 --- Tests whether the given host responds to ping probes.
331 -- @param host  String containing a hostname or IPv4 address
332 -- @return              Number containing 0 on success and >= 1 on error
333 function net.pingtest(host)
334         return os.execute("ping -c1 '"..host:gsub("'", '').."' >/dev/null 2>&1")
335 end
336
337
338 --- LuCI system utilities / process related functions.
339 -- @class       module
340 -- @name        luci.sys.process
341 process = {}
342
343 --- Get the current process id.
344 -- @class function
345 -- @name  process.info
346 -- @return      Number containing the current pid
347 process.info = posix.getpid
348
349 --- Retrieve information about currently running processes.
350 -- @return      Table containing process information
351 function process.list()
352         local data = {}
353         local k
354         local ps = luci.util.execi("top -bn1")
355
356         if not ps then
357                 return
358         end
359
360         while true do
361                 local line = ps()
362                 if not line then
363                         return
364                 end
365
366                 k = luci.util.split(luci.util.trim(line), "%s+", nil, true)
367                 if k[1] == "PID" then
368                         break
369                 end
370         end
371
372         for line in ps do
373                 local row = {}
374
375                 line = luci.util.trim(line)
376                 for i, value in ipairs(luci.util.split(line, "%s+", #k-1, true)) do
377                         row[k[i]] = value
378                 end
379
380                 local pid = tonumber(row[k[1]])
381                 if pid then
382                         data[pid] = row
383                 end
384         end
385
386         return data
387 end
388
389 --- Set the gid of a process identified by given pid.
390 -- @param pid   Number containing the process id
391 -- @param gid   Number containing the Unix group id
392 -- @return              Boolean indicating successful operation
393 -- @return              String containing the error message if failed
394 -- @return              Number containing the error code if failed
395 function process.setgroup(pid, gid)
396         return posix.setpid("g", pid, gid)
397 end
398
399 --- Set the uid of a process identified by given pid.
400 -- @param pid   Number containing the process id
401 -- @param uid   Number containing the Unix user id
402 -- @return              Boolean indicating successful operation
403 -- @return              String containing the error message if failed
404 -- @return              Number containing the error code if failed
405 function process.setuser(pid, uid)
406         return posix.setpid("u", pid, uid)
407 end
408
409 --- Send a signal to a process identified by given pid.
410 -- @class function
411 -- @name  process.signal
412 -- @param pid   Number containing the process id
413 -- @param sig   Signal to send (default: 15 [SIGTERM])
414 -- @return              Boolean indicating successful operation
415 -- @return              Number containing the error code if failed
416 process.signal = posix.kill
417
418
419 --- LuCI system utilities / user related functions.
420 -- @class       module
421 -- @name        luci.sys.user
422 user = {}
423
424 --- Retrieve user informations for given uid.
425 -- @class               function
426 -- @name                getuser
427 -- @param uid   Number containing the Unix user id
428 -- @return              Table containing the following fields:
429 --                              { "uid", "gid", "name", "passwd", "dir", "shell", "gecos" }
430 user.getuser = posix.getpasswd
431
432 --- Test whether given string matches the password of a given system user.
433 -- @param username      String containing the Unix user name
434 -- @param password      String containing the password to compare
435 -- @return                      Boolean indicating wheather the passwords are equal
436 function user.checkpasswd(username, password)
437         local account = user.getuser(username)
438
439         if account then
440                 local pwd = account.passwd
441                 local shadowpw
442                 if #pwd == 1 then
443                         if luci.fs.stat("/etc/shadow") then
444                                 if not pcall(function()
445                                         for l in io.lines("/etc/shadow") do
446                                                 shadowpw = l:match("^%s:([^:]+)" % username)
447                                                 if shadowpw then
448                                                         pwd = shadowpw
449                                                         break
450                                                 end
451                                         end
452                                 end) then
453                                         return nil, "Unable to access shadow-file"
454                                 end
455                         end
456
457                         if pwd == "!" then
458                                 return true
459                         end
460                 end
461
462                 return (pwd == posix.crypt(password, pwd))
463         end
464 end
465
466 --- Change the password of given user.
467 -- @param username      String containing the Unix user name
468 -- @param password      String containing the password to compare
469 -- @return                      Number containing 0 on success and >= 1 on error
470 function user.setpasswd(username, password)
471         if password then
472                 password = password:gsub("'", "")
473         end
474
475         if username then
476                 username = username:gsub("'", "")
477         end
478
479         local cmd = "(echo '"..password.."';sleep 1;echo '"..password.."')|"
480         cmd = cmd .. "passwd '"..username.."' >/dev/null 2>&1"
481         return os.execute(cmd)
482 end
483
484
485 --- LuCI system utilities / wifi related functions.
486 -- @class       module
487 -- @name        luci.sys.wifi
488 wifi = {}
489
490 --- Get iwconfig output for all wireless devices.
491 -- @return      Table of tables containing the iwconfing output for each wifi device
492 function wifi.getiwconfig()
493         local cnt = luci.util.exec("/usr/sbin/iwconfig 2>/dev/null")
494         local iwc = {}
495
496         for i, l in pairs(luci.util.split(luci.util.trim(cnt), "\n\n")) do
497                 local k = l:match("^(.-) ")
498                 l = l:gsub("^(.-) +", "", 1)
499                 if k then
500                         iwc[k] = _parse_mixed_record(l)
501                 end
502         end
503
504         return iwc
505 end
506
507 --- Get iwlist scan output from all wireless devices.
508 -- @return      Table of tables contaiing all scan results
509 function wifi.iwscan(iface)
510         local siface = iface or ""
511         local cnt = luci.util.exec("iwlist "..siface.." scan 2>/dev/null")
512         local iws = {}
513
514         for i, l in pairs(luci.util.split(luci.util.trim(cnt), "\n\n")) do
515                 local k = l:match("^(.-) ")
516                 l = l:gsub("^[^\n]+", "", 1)
517                 l = luci.util.trim(l)
518                 if k then
519                         iws[k] = {}
520                         for j, c in pairs(luci.util.split(l, "\n          Cell")) do
521                                 c = c:gsub("^(.-)- ", "", 1)
522                                 c = luci.util.split(c, "\n", 7)
523                                 c = table.concat(c, "\n", 1)
524                                 table.insert(iws[k], _parse_mixed_record(c))
525                         end
526                 end
527         end
528
529         return iface and (iws[iface] or {}) or iws
530 end
531
532
533 --- LuCI system utilities / init related functions.
534 -- @class       module
535 -- @name        luci.sys.init
536 init = {}
537 init.dir = "/etc/init.d/"
538
539 --- Get the names of all installed init scripts
540 -- @return      Table containing the names of all inistalled init scripts
541 function init.names()
542         local names = { }
543         for _, name in ipairs(luci.fs.glob(init.dir.."*")) do
544                 names[#names+1] = luci.fs.basename(name)
545         end
546         return names
547 end
548
549 --- Test whether the given init script is enabled
550 -- @param name  Name of the init script
551 -- @return              Boolean indicating whether init is enabled
552 function init.enabled(name)
553         if luci.fs.access(init.dir..name) then
554                 return ( call(init.dir..name.." enabled") == 0 )
555         end
556         return false
557 end
558
559 --- Get the index of he given init script
560 -- @param name  Name of the init script
561 -- @return              Numeric index value
562 function init.index(name)
563         if luci.fs.access(init.dir..name) then
564                 return call("source "..init.dir..name.."; exit $START")
565         end
566 end
567
568 --- Enable the given init script
569 -- @param name  Name of the init script
570 -- @return              Boolean indicating success
571 function init.enable(name)
572         if luci.fs.access(init.dir..name) then
573                 return ( call(init.dir..name.." enable") == 1 )
574         end
575 end
576
577 --- Disable the given init script
578 -- @param name  Name of the init script
579 -- @return              Boolean indicating success
580 function init.disable(name)
581         if luci.fs.access(init.dir..name) then
582                 return ( call(init.dir..name.." disable") == 0 )
583         end
584 end
585
586
587 -- Internal functions
588
589 function _parse_delimited_table(iter, delimiter)
590         delimiter = delimiter or "%s+"
591
592         local data  = {}
593         local trim  = luci.util.trim
594         local split = luci.util.split
595
596         local keys = split(trim(iter()), delimiter, nil, true)
597         for i, j in pairs(keys) do
598                 keys[i] = trim(keys[i])
599         end
600
601         for line in iter do
602                 local row = {}
603                 line = trim(line)
604                 if #line > 0 then
605                         for i, j in pairs(split(line, delimiter, nil, true)) do
606                                 if keys[i] then
607                                         row[keys[i]] = j
608                                 end
609                         end
610                 end
611                 table.insert(data, row)
612         end
613
614         return data
615 end
616
617 function _parse_mixed_record(cnt)
618         local data = {}
619
620         for i, l in pairs(luci.util.split(luci.util.trim(cnt), "\n")) do
621         for j, f in pairs(luci.util.split(luci.util.trim(l), "  ")) do
622                 local k, x, v = f:match('([^%s][^:=]+) *([:=]*) *"*([^\n"]*)"*')
623
624             if k then
625                                 if x == "" then
626                                         table.insert(data, k)
627                                 else
628                         data[k] = v
629                                 end
630             end
631         end
632         end
633
634     return data
635 end