libs/sys: extend luci.sys.hostname() to allow setting a new name
[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 --- Get or set the current hostname.
133 -- @param String containing a new hostname to set (optional)
134 -- @return String containing the system hostname
135 function hostname(newname)
136 if type(newname) == "string" and #newname > 0 then
137 luci.fs.writefile( "/proc/sys/kernel/hostname", newname .. "\n" )
138 return newname
139 else
140 return posix.uname("%n")
141 end
142 end
143
144 --- Returns the contents of a documented referred by an URL.
145 -- @param url The URL to retrieve
146 -- @param stream Return a stream instead of a buffer
147 -- @param target Directly write to target file name
148 -- @return String containing the contents of given the URL
149 function httpget(url, stream, target)
150 if not target then
151 local source = stream and io.popen or luci.util.exec
152 return source("wget -qO- '"..url:gsub("'", "").."'")
153 else
154 return os.execute("wget -qO '%s' '%s'" %
155 {target:gsub("'", ""), url:gsub("'", "")})
156 end
157 end
158
159 --- Returns the system load average values.
160 -- @return String containing the average load value 1 minute ago
161 -- @return String containing the average load value 5 minutes ago
162 -- @return String containing the average load value 15 minutes ago
163 -- @return String containing the active and total number of processes
164 -- @return String containing the last used pid
165 function loadavg()
166 local loadavg = io.lines("/proc/loadavg")()
167 return loadavg:match("^(.-) (.-) (.-) (.-) (.-)$")
168 end
169
170 --- Initiate a system reboot.
171 -- @return Return value of os.execute()
172 function reboot()
173 return os.execute("reboot >/dev/null 2>&1")
174 end
175
176 --- Returns the system type, cpu name and installed physical memory.
177 -- @return String containing the system or platform identifier
178 -- @return String containing hardware model information
179 -- @return String containing the total memory amount in kB
180 -- @return String containing the memory used for caching in kB
181 -- @return String containing the memory used for buffering in kB
182 -- @return String containing the free memory amount in kB
183 function sysinfo()
184 local cpuinfo = luci.fs.readfile("/proc/cpuinfo")
185 local meminfo = luci.fs.readfile("/proc/meminfo")
186
187 local system = cpuinfo:match("system typ.-:%s*([^\n]+)")
188 local model = ""
189 local memtotal = tonumber(meminfo:match("MemTotal:%s*(%d+)"))
190 local memcached = tonumber(meminfo:match("\nCached:%s*(%d+)"))
191 local memfree = tonumber(meminfo:match("MemFree:%s*(%d+)"))
192 local membuffers = tonumber(meminfo:match("Buffers:%s*(%d+)"))
193
194 if not system then
195 system = posix.uname("%m")
196 model = cpuinfo:match("model name.-:%s*([^\n]+)")
197 if not model then
198 model = cpuinfo:match("Processor.-:%s*([^\n]+)")
199 end
200 else
201 model = cpuinfo:match("cpu model.-:%s*([^\n]+)")
202 end
203
204 return system, model, memtotal, memcached, membuffers, memfree
205 end
206
207 --- Retrieves the output of the "logread" command.
208 -- @return String containing the current log buffer
209 function syslog()
210 return luci.util.exec("logread")
211 end
212
213 --- Retrieves the output of the "dmesg" command.
214 -- @return String containing the current log buffer
215 function dmesg()
216 return luci.util.exec("dmesg")
217 end
218
219 --- Generates a random id with specified length.
220 -- @param bytes Number of bytes for the unique id
221 -- @return String containing hex encoded id
222 function uniqueid(bytes)
223 local fp = io.open("/dev/urandom")
224 local chunk = { fp:read(bytes):byte(1, bytes) }
225 fp:close()
226
227 local hex = ""
228
229 local pattern = "%02X"
230 for i, byte in ipairs(chunk) do
231 hex = hex .. pattern:format(byte)
232 end
233
234 return hex
235 end
236
237 --- Returns the current system uptime stats.
238 -- @return String containing total uptime in seconds
239 -- @return String containing idle time in seconds
240 function uptime()
241 local loadavg = io.lines("/proc/uptime")()
242 return loadavg:match("^(.-) (.-)$")
243 end
244
245 --- LuCI system utilities / POSIX user group related functions.
246 -- @class module
247 -- @name luci.sys.group
248 group = {}
249
250 --- Returns information about a POSIX user group.
251 -- @class function
252 -- @name getgroup
253 -- @param group Group ID or name of a system user group
254 -- @return Table with information about the requested group
255 group.getgroup = posix.getgroup
256
257
258 --- LuCI system utilities / network related functions.
259 -- @class module
260 -- @name luci.sys.net
261 net = {}
262
263 --- Returns the current arp-table entries as two-dimensional table.
264 -- @return Table of table containing the current arp entries.
265 -- The following fields are defined for arp entry objects:
266 -- { "IP address", "HW address", "HW type", "Flags", "Mask", "Device" }
267 function net.arptable()
268 return _parse_delimited_table(io.lines("/proc/net/arp"), "%s%s+")
269 end
270
271 --- Returns conntrack information
272 -- @return Table with the currently tracked IP connections
273 function net.conntrack()
274 local connt = {}
275 if luci.fs.access("/proc/net/nf_conntrack", "r") then
276 for line in io.lines("/proc/net/nf_conntrack") do
277 line = line:match "^(.-( [^ =]+=).-)%2"
278 local entry, flags = _parse_mixed_record(line, " +")
279 entry.layer3 = flags[1]
280 entry.layer4 = flags[3]
281 for i=1, #entry do
282 entry[i] = nil
283 end
284
285 connt[#connt+1] = entry
286 end
287 elseif luci.fs.access("/proc/net/ip_conntrack", "r") then
288 for line in io.lines("/proc/net/ip_conntrack") do
289 line = line:match "^(.-( [^ =]+=).-)%2"
290 local entry, flags = _parse_mixed_record(line, " +")
291 entry.layer3 = "ipv4"
292 entry.layer4 = flags[1]
293 for i=1, #entry do
294 entry[i] = nil
295 end
296
297 connt[#connt+1] = entry
298 end
299 else
300 return nil
301 end
302 return connt
303 end
304
305 --- Determine the current IPv4 default route. If multiple default routes exist,
306 -- return the one with the lowest metric.
307 -- @return Table with the properties of the current default route.
308 -- The following fields are defined:
309 -- { "dest", "gateway", "metric", "refcount", "usecount", "irtt",
310 -- "flags", "device" }
311 function net.defaultroute()
312 local route = nil
313 for _, r in pairs(net.routes()) do
314 if r.dest:prefix() == 0 and (not route or route.metric > r.metric) then
315 route = r
316 end
317 end
318 return route
319 end
320
321 --- Determine the current IPv6 default route. If multiple default routes exist,
322 -- return the one with the lowest metric.
323 -- @return Table with the properties of the current default route.
324 -- The following fields are defined:
325 -- { "source", "dest", "nexthop", "metric", "refcount", "usecount",
326 -- "flags", "device" }
327 function net.defaultroute6()
328 local route = nil
329 local routes6 = net.routes6()
330 if routes6 then
331 for _, r in pairs(routes6) do
332 if r.dest:prefix() == 0 and
333 (not route or route.metric > r.metric)
334 then
335 route = r
336 end
337 end
338 end
339 return route
340 end
341
342 --- Determine the names of available network interfaces.
343 -- @return Table containing all current interface names
344 function net.devices()
345 local devices = {}
346 for line in io.lines("/proc/net/dev") do
347 table.insert(devices, line:match(" *(.-):"))
348 end
349 return devices
350 end
351
352
353 --- Return information about available network interfaces.
354 -- @return Table containing all current interface names and their information
355 function net.deviceinfo()
356 local devices = {}
357 for line in io.lines("/proc/net/dev") do
358 local name, data = line:match("^ *(.-): *(.*)$")
359 if name and data then
360 devices[name] = luci.util.split(data, " +", nil, true)
361 end
362 end
363 return devices
364 end
365
366
367 -- Determine the MAC address belonging to the given IP address.
368 -- @param ip IPv4 address
369 -- @return String containing the MAC address or nil if it cannot be found
370 function net.ip4mac(ip)
371 local mac = nil
372
373 for i, l in ipairs(net.arptable()) do
374 if l["IP address"] == ip then
375 mac = l["HW address"]
376 end
377 end
378
379 return mac
380 end
381
382 --- Returns the current kernel routing table entries.
383 -- @return Table of tables with properties of the corresponding routes.
384 -- The following fields are defined for route entry tables:
385 -- { "dest", "gateway", "metric", "refcount", "usecount", "irtt",
386 -- "flags", "device" }
387 function net.routes()
388 local routes = { }
389
390 for line in io.lines("/proc/net/route") do
391
392 local dev, dst_ip, gateway, flags, refcnt, usecnt, metric,
393 dst_mask, mtu, win, irtt = line:match(
394 "([^%s]+)\t([A-F0-9]+)\t([A-F0-9]+)\t([A-F0-9]+)\t" ..
395 "(%d+)\t(%d+)\t(%d+)\t([A-F0-9]+)\t(%d+)\t(%d+)\t(%d+)"
396 )
397
398 if dev then
399 gateway = luci.ip.Hex( gateway, 32, luci.ip.FAMILY_INET4 )
400 dst_mask = luci.ip.Hex( dst_mask, 32, luci.ip.FAMILY_INET4 )
401 dst_ip = luci.ip.Hex(
402 dst_ip, dst_mask:prefix(dst_mask), luci.ip.FAMILY_INET4
403 )
404
405 routes[#routes+1] = {
406 dest = dst_ip,
407 gateway = gateway,
408 metric = tonumber(metric),
409 refcount = tonumber(refcnt),
410 usecount = tonumber(usecnt),
411 mtu = tonumber(mtu),
412 window = tonumber(window),
413 irtt = tonumber(irtt),
414 flags = tonumber(flags, 16),
415 device = dev
416 }
417 end
418 end
419
420 return routes
421 end
422
423 --- Returns the current ipv6 kernel routing table entries.
424 -- @return Table of tables with properties of the corresponding routes.
425 -- The following fields are defined for route entry tables:
426 -- { "source", "dest", "nexthop", "metric", "refcount", "usecount",
427 -- "flags", "device" }
428 function net.routes6()
429 if luci.fs.access("/proc/net/ipv6_route", "r") then
430 local routes = { }
431
432 for line in io.lines("/proc/net/ipv6_route") do
433
434 local dst_ip, dst_prefix, src_ip, src_prefix, nexthop,
435 metric, refcnt, usecnt, flags, dev = line:match(
436 "([a-f0-9]+) ([a-f0-9]+) " ..
437 "([a-f0-9]+) ([a-f0-9]+) " ..
438 "([a-f0-9]+) ([a-f0-9]+) " ..
439 "([a-f0-9]+) ([a-f0-9]+) " ..
440 "([a-f0-9]+) +([^%s]+)"
441 )
442
443 src_ip = luci.ip.Hex(
444 src_ip, tonumber(src_prefix, 16), luci.ip.FAMILY_INET6, false
445 )
446
447 dst_ip = luci.ip.Hex(
448 dst_ip, tonumber(dst_prefix, 16), luci.ip.FAMILY_INET6, false
449 )
450
451 nexthop = luci.ip.Hex( nexthop, 128, luci.ip.FAMILY_INET6, false )
452
453 routes[#routes+1] = {
454 source = src_ip,
455 dest = dst_ip,
456 nexthop = nexthop,
457 metric = tonumber(metric, 16),
458 refcount = tonumber(refcnt, 16),
459 usecount = tonumber(usecnt, 16),
460 flags = tonumber(flags, 16),
461 device = dev
462 }
463 end
464
465 return routes
466 end
467 end
468
469 --- Tests whether the given host responds to ping probes.
470 -- @param host String containing a hostname or IPv4 address
471 -- @return Number containing 0 on success and >= 1 on error
472 function net.pingtest(host)
473 return os.execute("ping -c1 '"..host:gsub("'", '').."' >/dev/null 2>&1")
474 end
475
476
477 --- LuCI system utilities / process related functions.
478 -- @class module
479 -- @name luci.sys.process
480 process = {}
481
482 --- Get the current process id.
483 -- @class function
484 -- @name process.info
485 -- @return Number containing the current pid
486 process.info = posix.getpid
487
488 --- Retrieve information about currently running processes.
489 -- @return Table containing process information
490 function process.list()
491 local data = {}
492 local k
493 local ps = luci.util.execi("top -bn1")
494
495 if not ps then
496 return
497 end
498
499 while true do
500 local line = ps()
501 if not line then
502 return
503 end
504
505 k = luci.util.split(luci.util.trim(line), "%s+", nil, true)
506 if k[1] == "PID" then
507 break
508 end
509 end
510
511 for line in ps do
512 local row = {}
513
514 line = luci.util.trim(line)
515 for i, value in ipairs(luci.util.split(line, "%s+", #k-1, true)) do
516 row[k[i]] = value
517 end
518
519 local pid = tonumber(row[k[1]])
520 if pid then
521 data[pid] = row
522 end
523 end
524
525 return data
526 end
527
528 --- Set the gid of a process identified by given pid.
529 -- @param pid Number containing the process id
530 -- @param gid Number containing the Unix group id
531 -- @return Boolean indicating successful operation
532 -- @return String containing the error message if failed
533 -- @return Number containing the error code if failed
534 function process.setgroup(pid, gid)
535 return posix.setpid("g", pid, gid)
536 end
537
538 --- Set the uid of a process identified by given pid.
539 -- @param pid Number containing the process id
540 -- @param uid Number containing the Unix user id
541 -- @return Boolean indicating successful operation
542 -- @return String containing the error message if failed
543 -- @return Number containing the error code if failed
544 function process.setuser(pid, uid)
545 return posix.setpid("u", pid, uid)
546 end
547
548 --- Send a signal to a process identified by given pid.
549 -- @class function
550 -- @name process.signal
551 -- @param pid Number containing the process id
552 -- @param sig Signal to send (default: 15 [SIGTERM])
553 -- @return Boolean indicating successful operation
554 -- @return Number containing the error code if failed
555 process.signal = posix.kill
556
557
558 --- LuCI system utilities / user related functions.
559 -- @class module
560 -- @name luci.sys.user
561 user = {}
562
563 --- Retrieve user informations for given uid.
564 -- @class function
565 -- @name getuser
566 -- @param uid Number containing the Unix user id
567 -- @return Table containing the following fields:
568 -- { "uid", "gid", "name", "passwd", "dir", "shell", "gecos" }
569 user.getuser = posix.getpasswd
570
571 --- Test whether given string matches the password of a given system user.
572 -- @param username String containing the Unix user name
573 -- @param password String containing the password to compare
574 -- @return Boolean indicating wheather the passwords are equal
575 function user.checkpasswd(username, password)
576 local account = user.getuser(username)
577
578 if account then
579 local pwd = account.passwd
580 local shadowpw
581 if #pwd == 1 then
582 if luci.fs.stat("/etc/shadow") then
583 if not pcall(function()
584 for l in io.lines("/etc/shadow") do
585 shadowpw = l:match("^%s:([^:]+)" % username)
586 if shadowpw then
587 pwd = shadowpw
588 break
589 end
590 end
591 end) then
592 return nil, "Unable to access shadow-file"
593 end
594 end
595
596 if pwd == "!" then
597 return true
598 end
599 end
600
601 if pwd and #pwd > 0 and password and #password > 0 then
602 return (pwd == posix.crypt(password, pwd))
603 end
604 end
605
606 return false
607 end
608
609 --- Change the password of given user.
610 -- @param username String containing the Unix user name
611 -- @param password String containing the password to compare
612 -- @return Number containing 0 on success and >= 1 on error
613 function user.setpasswd(username, password)
614 if password then
615 password = password:gsub("'", "")
616 end
617
618 if username then
619 username = username:gsub("'", "")
620 end
621
622 local cmd = "(echo '"..password.."';sleep 1;echo '"..password.."')|"
623 cmd = cmd .. "passwd '"..username.."' >/dev/null 2>&1"
624 return os.execute(cmd)
625 end
626
627
628 --- LuCI system utilities / wifi related functions.
629 -- @class module
630 -- @name luci.sys.wifi
631 wifi = {}
632
633 --- Get iwconfig output for all wireless devices.
634 -- @return Table of tables containing the iwconfing output for each wifi device
635 function wifi.getiwconfig()
636 local cnt = luci.util.exec("PATH=/sbin:/usr/sbin iwconfig 2>/dev/null")
637 local iwc = {}
638
639 for i, l in pairs(luci.util.split(luci.util.trim(cnt), "\n\n")) do
640 local k = l:match("^(.-) ")
641 l = l:gsub("^(.-) +", "", 1)
642 if k then
643 local entry, flags = _parse_mixed_record(l)
644 if entry then
645 entry.flags = flags
646 end
647 iwc[k] = entry
648 end
649 end
650
651 return iwc
652 end
653
654 --- Get iwlist scan output from all wireless devices.
655 -- @return Table of tables contaiing all scan results
656 function wifi.iwscan(iface)
657 local siface = iface or ""
658 local cnt = luci.util.exec("iwlist "..siface.." scan 2>/dev/null")
659 local iws = {}
660
661 for i, l in pairs(luci.util.split(luci.util.trim(cnt), "\n\n")) do
662 local k = l:match("^(.-) ")
663 l = l:gsub("^[^\n]+", "", 1)
664 l = luci.util.trim(l)
665 if k then
666 iws[k] = {}
667 for j, c in pairs(luci.util.split(l, "\n Cell")) do
668 c = c:gsub("^(.-)- ", "", 1)
669 c = luci.util.split(c, "\n", 7)
670 c = table.concat(c, "\n", 1)
671 local entry, flags = _parse_mixed_record(c)
672 if entry then
673 entry.flags = flags
674 end
675 table.insert(iws[k], entry)
676 end
677 end
678 end
679
680 return iface and (iws[iface] or {}) or iws
681 end
682
683
684 --- LuCI system utilities / init related functions.
685 -- @class module
686 -- @name luci.sys.init
687 init = {}
688 init.dir = "/etc/init.d/"
689
690 --- Get the names of all installed init scripts
691 -- @return Table containing the names of all inistalled init scripts
692 function init.names()
693 local names = { }
694 for _, name in ipairs(luci.fs.glob(init.dir.."*")) do
695 names[#names+1] = luci.fs.basename(name)
696 end
697 return names
698 end
699
700 --- Test whether the given init script is enabled
701 -- @param name Name of the init script
702 -- @return Boolean indicating whether init is enabled
703 function init.enabled(name)
704 if luci.fs.access(init.dir..name) then
705 return ( call(init.dir..name.." enabled") == 0 )
706 end
707 return false
708 end
709
710 --- Get the index of he given init script
711 -- @param name Name of the init script
712 -- @return Numeric index value
713 function init.index(name)
714 if luci.fs.access(init.dir..name) then
715 return call("source "..init.dir..name.."; exit $START")
716 end
717 end
718
719 --- Enable the given init script
720 -- @param name Name of the init script
721 -- @return Boolean indicating success
722 function init.enable(name)
723 if luci.fs.access(init.dir..name) then
724 return ( call(init.dir..name.." enable") == 1 )
725 end
726 end
727
728 --- Disable the given init script
729 -- @param name Name of the init script
730 -- @return Boolean indicating success
731 function init.disable(name)
732 if luci.fs.access(init.dir..name) then
733 return ( call(init.dir..name.." disable") == 0 )
734 end
735 end
736
737
738 -- Internal functions
739
740 function _parse_delimited_table(iter, delimiter)
741 delimiter = delimiter or "%s+"
742
743 local data = {}
744 local trim = luci.util.trim
745 local split = luci.util.split
746
747 local keys = split(trim(iter()), delimiter, nil, true)
748 for i, j in pairs(keys) do
749 keys[i] = trim(keys[i])
750 end
751
752 for line in iter do
753 local row = {}
754 line = trim(line)
755 if #line > 0 then
756 for i, j in pairs(split(line, delimiter, nil, true)) do
757 if keys[i] then
758 row[keys[i]] = j
759 end
760 end
761 end
762 table.insert(data, row)
763 end
764
765 return data
766 end
767
768 function _parse_mixed_record(cnt, delimiter)
769 delimiter = delimiter or " "
770 local data = {}
771 local flags = {}
772
773 for i, l in pairs(luci.util.split(luci.util.trim(cnt), "\n")) do
774 for j, f in pairs(luci.util.split(luci.util.trim(l), delimiter, nil, true)) do
775 local k, x, v = f:match('([^%s][^:=]*) *([:=]*) *"*([^\n"]*)"*')
776
777 if k then
778 if x == "" then
779 table.insert(flags, k)
780 else
781 data[k] = v
782 end
783 end
784 end
785 end
786
787 return data, flags
788 end