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