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