Replace some makes-you-want-to-slash-your-wrists-Code with something less harmful
[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         else
192                 model = cpuinfo:match("cpu model.-:%s*([^\n]+)")
193         end
194
195         return system, model, memtotal, memcached, membuffers, memfree
196 end
197
198 --- Retrieves the output of the "logread" command.
199 -- @return      String containing the current log buffer
200 function syslog()
201         return luci.util.exec("logread")
202 end
203
204 --- Generates a random id with specified length.
205 -- @param bytes Number of bytes for the unique id
206 -- @return              String containing hex encoded id
207 function uniqueid(bytes)
208         local fp    = io.open("/dev/urandom")
209         local chunk = { fp:read(bytes):byte(1, bytes) }
210         fp:close()
211
212         local hex = ""
213
214         local pattern = "%02X"
215         for i, byte in ipairs(chunk) do
216                 hex = hex .. pattern:format(byte)
217         end
218
219         return hex
220 end
221
222 --- Returns the current system uptime stats.
223 -- @return      String containing total uptime in seconds
224 -- @return      String containing idle time in seconds
225 function uptime()
226         local loadavg = io.lines("/proc/uptime")()
227         return loadavg:match("^(.-) (.-)$")
228 end
229
230 --- LuCI system utilities / POSIX user group related functions.
231 -- @class       module
232 -- @name        luci.sys.group
233 group = {}
234
235 --- Returns information about a POSIX user group.
236 -- @class function
237 -- @name                getgroup
238 -- @param group Group ID or name of a system user group
239 -- @return      Table with information about the requested group
240 group.getgroup = posix.getgroup
241
242
243 --- LuCI system utilities / network related functions.
244 -- @class       module
245 -- @name        luci.sys.net
246 net = {}
247
248 --- Returns the current arp-table entries as two-dimensional table.
249 -- @return      Table of table containing the current arp entries.
250 --                      The following fields are defined for arp entry objects:
251 --                      { "IP address", "HW address", "HW type", "Flags", "Mask", "Device" }
252 function net.arptable()
253         return _parse_delimited_table(io.lines("/proc/net/arp"), "%s%s+")
254 end
255
256 --- Returns conntrack information
257 -- @return      Table with the currently tracked IP connections
258 function net.conntrack()
259         local connt = {}
260         if luci.fs.access("/proc/net/nf_conntrack") then
261                 for line in io.lines("/proc/net/nf_conntrack") do
262                         local entry, flags = _parse_mixed_record(line, " +")
263                         entry.layer3 = flags[1]
264                         entry.layer4 = flags[2]
265                         for i=1, #entry do
266                                 entry[i] = nil
267                         end
268
269                         connt[#connt+1] = entry
270                 end
271         elseif luci.fs.access("/proc/net/ip_conntrack") then
272                 for line in io.lines("/proc/net/ip_conntrack") do
273                         local entry, flags = _parse_mixed_record(line, " +")
274                         entry.layer3 = "ipv4"
275                         entry.layer4 = flags[1]
276                         for i=1, #entry do
277                                 entry[i] = nil
278                         end
279
280                         connt[#connt+1] = entry
281                 end
282         else
283                 return nil
284         end
285         return connt
286 end
287
288 --- Determine the current default route.
289 -- @return      Table with the properties of the current default route.
290 --                      The following fields are defined:
291 --                      { "Mask", "RefCnt", "Iface", "Flags", "Window", "IRTT",
292 --                        "MTU", "Gateway", "Destination", "Metric", "Use" }
293 function net.defaultroute()
294         local routes = net.routes()
295         local route = nil
296
297         for i, r in pairs(luci.sys.net.routes()) do
298                 if r.Destination == "00000000" and (not route or route.Metric > r.Metric) then
299                         route = r
300                 end
301         end
302
303         return route
304 end
305
306 --- Determine the names of available network interfaces.
307 -- @return      Table containing all current interface names
308 function net.devices()
309         local devices = {}
310         for line in io.lines("/proc/net/dev") do
311                 table.insert(devices, line:match(" *(.-):"))
312         end
313         return devices
314 end
315
316
317 --- Return information about available network interfaces.
318 -- @return      Table containing all current interface names and their information
319 function net.deviceinfo()
320         local devices = {}
321         for line in io.lines("/proc/net/dev") do
322                 local name, data = line:match("^ *(.-): *(.*)$")
323                 if name and data then
324                         devices[name] = luci.util.split(data, " +", nil, true)
325                 end
326         end
327         return devices
328 end
329
330
331 -- Determine the MAC address belonging to the given IP address.
332 -- @param ip    IPv4 address
333 -- @return              String containing the MAC address or nil if it cannot be found
334 function net.ip4mac(ip)
335         local mac = nil
336
337         for i, l in ipairs(net.arptable()) do
338                 if l["IP address"] == ip then
339                         mac = l["HW address"]
340                 end
341         end
342
343         return mac
344 end
345
346 --- Returns the current kernel routing table entries.
347 -- @return      Table of tables with properties of the corresponding routes.
348 --                      The following fields are defined for route entry tables:
349 --                      { "Mask", "RefCnt", "Iface", "Flags", "Window", "IRTT",
350 --                        "MTU", "Gateway", "Destination", "Metric", "Use" }
351 function net.routes()
352         return _parse_delimited_table(io.lines("/proc/net/route"))
353 end
354
355
356 --- Tests whether the given host responds to ping probes.
357 -- @param host  String containing a hostname or IPv4 address
358 -- @return              Number containing 0 on success and >= 1 on error
359 function net.pingtest(host)
360         return os.execute("ping -c1 '"..host:gsub("'", '').."' >/dev/null 2>&1")
361 end
362
363
364 --- LuCI system utilities / process related functions.
365 -- @class       module
366 -- @name        luci.sys.process
367 process = {}
368
369 --- Get the current process id.
370 -- @class function
371 -- @name  process.info
372 -- @return      Number containing the current pid
373 process.info = posix.getpid
374
375 --- Retrieve information about currently running processes.
376 -- @return      Table containing process information
377 function process.list()
378         local data = {}
379         local k
380         local ps = luci.util.execi("top -bn1")
381
382         if not ps then
383                 return
384         end
385
386         while true do
387                 local line = ps()
388                 if not line then
389                         return
390                 end
391
392                 k = luci.util.split(luci.util.trim(line), "%s+", nil, true)
393                 if k[1] == "PID" then
394                         break
395                 end
396         end
397
398         for line in ps do
399                 local row = {}
400
401                 line = luci.util.trim(line)
402                 for i, value in ipairs(luci.util.split(line, "%s+", #k-1, true)) do
403                         row[k[i]] = value
404                 end
405
406                 local pid = tonumber(row[k[1]])
407                 if pid then
408                         data[pid] = row
409                 end
410         end
411
412         return data
413 end
414
415 --- Set the gid of a process identified by given pid.
416 -- @param pid   Number containing the process id
417 -- @param gid   Number containing the Unix group id
418 -- @return              Boolean indicating successful operation
419 -- @return              String containing the error message if failed
420 -- @return              Number containing the error code if failed
421 function process.setgroup(pid, gid)
422         return posix.setpid("g", pid, gid)
423 end
424
425 --- Set the uid of a process identified by given pid.
426 -- @param pid   Number containing the process id
427 -- @param uid   Number containing the Unix user id
428 -- @return              Boolean indicating successful operation
429 -- @return              String containing the error message if failed
430 -- @return              Number containing the error code if failed
431 function process.setuser(pid, uid)
432         return posix.setpid("u", pid, uid)
433 end
434
435 --- Send a signal to a process identified by given pid.
436 -- @class function
437 -- @name  process.signal
438 -- @param pid   Number containing the process id
439 -- @param sig   Signal to send (default: 15 [SIGTERM])
440 -- @return              Boolean indicating successful operation
441 -- @return              Number containing the error code if failed
442 process.signal = posix.kill
443
444
445 --- LuCI system utilities / user related functions.
446 -- @class       module
447 -- @name        luci.sys.user
448 user = {}
449
450 --- Retrieve user informations for given uid.
451 -- @class               function
452 -- @name                getuser
453 -- @param uid   Number containing the Unix user id
454 -- @return              Table containing the following fields:
455 --                              { "uid", "gid", "name", "passwd", "dir", "shell", "gecos" }
456 user.getuser = posix.getpasswd
457
458 --- Test whether given string matches the password of a given system user.
459 -- @param username      String containing the Unix user name
460 -- @param password      String containing the password to compare
461 -- @return                      Boolean indicating wheather the passwords are equal
462 function user.checkpasswd(username, password)
463         local account = user.getuser(username)
464
465         if account then
466                 local pwd = account.passwd
467                 local shadowpw
468                 if #pwd == 1 then
469                         if luci.fs.stat("/etc/shadow") then
470                                 if not pcall(function()
471                                         for l in io.lines("/etc/shadow") do
472                                                 shadowpw = l:match("^%s:([^:]+)" % username)
473                                                 if shadowpw then
474                                                         pwd = shadowpw
475                                                         break
476                                                 end
477                                         end
478                                 end) then
479                                         return nil, "Unable to access shadow-file"
480                                 end
481                         end
482
483                         if pwd == "!" then
484                                 return true
485                         end
486                 end
487
488                 if pwd and #pwd > 0 and password and #password > 0 then
489                         return (pwd == posix.crypt(password, pwd))
490                 end
491         end
492
493         return false
494 end
495
496 --- Change the password of given user.
497 -- @param username      String containing the Unix user name
498 -- @param password      String containing the password to compare
499 -- @return                      Number containing 0 on success and >= 1 on error
500 function user.setpasswd(username, password)
501         if password then
502                 password = password:gsub("'", "")
503         end
504
505         if username then
506                 username = username:gsub("'", "")
507         end
508
509         local cmd = "(echo '"..password.."';sleep 1;echo '"..password.."')|"
510         cmd = cmd .. "passwd '"..username.."' >/dev/null 2>&1"
511         return os.execute(cmd)
512 end
513
514
515 --- LuCI system utilities / wifi related functions.
516 -- @class       module
517 -- @name        luci.sys.wifi
518 wifi = {}
519
520 --- Get iwconfig output for all wireless devices.
521 -- @return      Table of tables containing the iwconfing output for each wifi device
522 function wifi.getiwconfig()
523         local cnt = luci.util.exec("/usr/sbin/iwconfig 2>/dev/null")
524         local iwc = {}
525
526         for i, l in pairs(luci.util.split(luci.util.trim(cnt), "\n\n")) do
527                 local k = l:match("^(.-) ")
528                 l = l:gsub("^(.-) +", "", 1)
529                 if k then
530                         local entry, flags = _parse_mixed_record(l)
531                         if entry then
532                                 entry.flags = flags
533                         end
534                         iwc[k] = entry
535                 end
536         end
537
538         return iwc
539 end
540
541 --- Get iwlist scan output from all wireless devices.
542 -- @return      Table of tables contaiing all scan results
543 function wifi.iwscan(iface)
544         local siface = iface or ""
545         local cnt = luci.util.exec("iwlist "..siface.." scan 2>/dev/null")
546         local iws = {}
547
548         for i, l in pairs(luci.util.split(luci.util.trim(cnt), "\n\n")) do
549                 local k = l:match("^(.-) ")
550                 l = l:gsub("^[^\n]+", "", 1)
551                 l = luci.util.trim(l)
552                 if k then
553                         iws[k] = {}
554                         for j, c in pairs(luci.util.split(l, "\n          Cell")) do
555                                 c = c:gsub("^(.-)- ", "", 1)
556                                 c = luci.util.split(c, "\n", 7)
557                                 c = table.concat(c, "\n", 1)
558                                 local entry, flags = _parse_mixed_record(c)
559                                 if entry then
560                                         entry.flags = flags
561                                 end
562                                 table.insert(iws[k], entry)
563                         end
564                 end
565         end
566
567         return iface and (iws[iface] or {}) or iws
568 end
569
570
571 --- LuCI system utilities / init related functions.
572 -- @class       module
573 -- @name        luci.sys.init
574 init = {}
575 init.dir = "/etc/init.d/"
576
577 --- Get the names of all installed init scripts
578 -- @return      Table containing the names of all inistalled init scripts
579 function init.names()
580         local names = { }
581         for _, name in ipairs(luci.fs.glob(init.dir.."*")) do
582                 names[#names+1] = luci.fs.basename(name)
583         end
584         return names
585 end
586
587 --- Test whether the given init script is enabled
588 -- @param name  Name of the init script
589 -- @return              Boolean indicating whether init is enabled
590 function init.enabled(name)
591         if luci.fs.access(init.dir..name) then
592                 return ( call(init.dir..name.." enabled") == 0 )
593         end
594         return false
595 end
596
597 --- Get the index of he given init script
598 -- @param name  Name of the init script
599 -- @return              Numeric index value
600 function init.index(name)
601         if luci.fs.access(init.dir..name) then
602                 return call("source "..init.dir..name.."; exit $START")
603         end
604 end
605
606 --- Enable the given init script
607 -- @param name  Name of the init script
608 -- @return              Boolean indicating success
609 function init.enable(name)
610         if luci.fs.access(init.dir..name) then
611                 return ( call(init.dir..name.." enable") == 1 )
612         end
613 end
614
615 --- Disable the given init script
616 -- @param name  Name of the init script
617 -- @return              Boolean indicating success
618 function init.disable(name)
619         if luci.fs.access(init.dir..name) then
620                 return ( call(init.dir..name.." disable") == 0 )
621         end
622 end
623
624
625 -- Internal functions
626
627 function _parse_delimited_table(iter, delimiter)
628         delimiter = delimiter or "%s+"
629
630         local data  = {}
631         local trim  = luci.util.trim
632         local split = luci.util.split
633
634         local keys = split(trim(iter()), delimiter, nil, true)
635         for i, j in pairs(keys) do
636                 keys[i] = trim(keys[i])
637         end
638
639         for line in iter do
640                 local row = {}
641                 line = trim(line)
642                 if #line > 0 then
643                         for i, j in pairs(split(line, delimiter, nil, true)) do
644                                 if keys[i] then
645                                         row[keys[i]] = j
646                                 end
647                         end
648                 end
649                 table.insert(data, row)
650         end
651
652         return data
653 end
654
655 function _parse_mixed_record(cnt, delimiter)
656         delimiter = delimiter or "  "
657         local data = {}
658         local flags = {}
659
660         for i, l in pairs(luci.util.split(luci.util.trim(cnt), "\n")) do
661                 for j, f in pairs(luci.util.split(luci.util.trim(l), delimiter, nil, true)) do
662                 local k, x, v = f:match('([^%s][^:=]+) *([:=]*) *"*([^\n"]*)"*')
663
664             if k then
665                                 if x == "" then
666                                         table.insert(flags, k)
667                                 else
668                         data[k] = v
669                                 end
670             end
671         end
672         end
673
674     return data, flags
675 end