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