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