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