| 1 | --[[ |
|---|
| 2 | LuCI - Lua Configuration Interface |
|---|
| 3 | |
|---|
| 4 | Copyright 2008 Steven Barth <steven@midlink.org> |
|---|
| 5 | Copyright 2008-2011 Jo-Philipp Wich <xm@subsignal.org> |
|---|
| 6 | |
|---|
| 7 | Licensed under the Apache License, Version 2.0 (the "License"); |
|---|
| 8 | you may not use this file except in compliance with the License. |
|---|
| 9 | You may obtain a copy of the License at |
|---|
| 10 | |
|---|
| 11 | http://www.apache.org/licenses/LICENSE-2.0 |
|---|
| 12 | |
|---|
| 13 | $Id$ |
|---|
| 14 | ]]-- |
|---|
| 15 | |
|---|
| 16 | module("luci.controller.admin.system", package.seeall) |
|---|
| 17 | |
|---|
| 18 | function index() |
|---|
| 19 | entry({"admin", "system"}, alias("admin", "system", "system"), _("System"), 30).index = true |
|---|
| 20 | entry({"admin", "system", "system"}, cbi("admin_system/system"), _("System"), 1) |
|---|
| 21 | entry({"admin", "system", "clock_status"}, call("action_clock_status")) |
|---|
| 22 | |
|---|
| 23 | entry({"admin", "system", "admin"}, cbi("admin_system/admin"), _("Administration"), 2) |
|---|
| 24 | |
|---|
| 25 | if nixio.fs.access("/bin/opkg") then |
|---|
| 26 | entry({"admin", "system", "packages"}, call("action_packages"), _("Software"), 10) |
|---|
| 27 | entry({"admin", "system", "packages", "ipkg"}, form("admin_system/ipkg")) |
|---|
| 28 | end |
|---|
| 29 | |
|---|
| 30 | entry({"admin", "system", "startup"}, form("admin_system/startup"), _("Startup"), 45) |
|---|
| 31 | entry({"admin", "system", "crontab"}, form("admin_system/crontab"), _("Scheduled Tasks"), 46) |
|---|
| 32 | |
|---|
| 33 | if nixio.fs.access("/etc/config/fstab") then |
|---|
| 34 | entry({"admin", "system", "fstab"}, cbi("admin_system/fstab"), _("Mount Points"), 50) |
|---|
| 35 | entry({"admin", "system", "fstab", "mount"}, cbi("admin_system/fstab/mount"), nil).leaf = true |
|---|
| 36 | entry({"admin", "system", "fstab", "swap"}, cbi("admin_system/fstab/swap"), nil).leaf = true |
|---|
| 37 | end |
|---|
| 38 | |
|---|
| 39 | if nixio.fs.access("/sys/class/leds") then |
|---|
| 40 | entry({"admin", "system", "leds"}, cbi("admin_system/leds"), _("<abbr title=\"Light Emitting Diode\">LED</abbr> Configuration"), 60) |
|---|
| 41 | end |
|---|
| 42 | |
|---|
| 43 | entry({"admin", "system", "flashops"}, call("action_flashops"), _("Backup / Flash Firmware"), 70) |
|---|
| 44 | entry({"admin", "system", "flashops", "backupfiles"}, form("admin_system/backupfiles")) |
|---|
| 45 | |
|---|
| 46 | entry({"admin", "system", "reboot"}, call("action_reboot"), _("Reboot"), 90) |
|---|
| 47 | end |
|---|
| 48 | |
|---|
| 49 | function action_clock_status() |
|---|
| 50 | local set = tonumber(luci.http.formvalue("set")) |
|---|
| 51 | if set ~= nil and set > 0 then |
|---|
| 52 | local date = os.date("*t", set) |
|---|
| 53 | if date then |
|---|
| 54 | luci.sys.call("date -s '%04d-%02d-%02d %02d:%02d:%02d'" %{ |
|---|
| 55 | date.year, date.month, date.day, date.hour, date.min, date.sec |
|---|
| 56 | }) |
|---|
| 57 | end |
|---|
| 58 | end |
|---|
| 59 | |
|---|
| 60 | luci.http.prepare_content("application/json") |
|---|
| 61 | luci.http.write_json({ timestring = os.date("%c") }) |
|---|
| 62 | end |
|---|
| 63 | |
|---|
| 64 | function action_packages() |
|---|
| 65 | local ipkg = require("luci.model.ipkg") |
|---|
| 66 | local submit = luci.http.formvalue("submit") |
|---|
| 67 | local changes = false |
|---|
| 68 | local install = { } |
|---|
| 69 | local remove = { } |
|---|
| 70 | local stdout = { "" } |
|---|
| 71 | local stderr = { "" } |
|---|
| 72 | local out, err |
|---|
| 73 | |
|---|
| 74 | -- Display |
|---|
| 75 | local display = luci.http.formvalue("display") or "installed" |
|---|
| 76 | |
|---|
| 77 | -- Letter |
|---|
| 78 | local letter = string.byte(luci.http.formvalue("letter") or "A", 1) |
|---|
| 79 | letter = (letter == 35 or (letter >= 65 and letter <= 90)) and letter or 65 |
|---|
| 80 | |
|---|
| 81 | -- Search query |
|---|
| 82 | local query = luci.http.formvalue("query") |
|---|
| 83 | query = (query ~= '') and query or nil |
|---|
| 84 | |
|---|
| 85 | |
|---|
| 86 | -- Packets to be installed |
|---|
| 87 | local ninst = submit and luci.http.formvalue("install") |
|---|
| 88 | local uinst = nil |
|---|
| 89 | |
|---|
| 90 | -- Install from URL |
|---|
| 91 | local url = luci.http.formvalue("url") |
|---|
| 92 | if url and url ~= '' and submit then |
|---|
| 93 | uinst = url |
|---|
| 94 | end |
|---|
| 95 | |
|---|
| 96 | -- Do install |
|---|
| 97 | if ninst then |
|---|
| 98 | install[ninst], out, err = ipkg.install(ninst) |
|---|
| 99 | stdout[#stdout+1] = out |
|---|
| 100 | stderr[#stderr+1] = err |
|---|
| 101 | changes = true |
|---|
| 102 | end |
|---|
| 103 | |
|---|
| 104 | if uinst then |
|---|
| 105 | local pkg |
|---|
| 106 | for pkg in luci.util.imatch(uinst) do |
|---|
| 107 | install[uinst], out, err = ipkg.install(pkg) |
|---|
| 108 | stdout[#stdout+1] = out |
|---|
| 109 | stderr[#stderr+1] = err |
|---|
| 110 | changes = true |
|---|
| 111 | end |
|---|
| 112 | end |
|---|
| 113 | |
|---|
| 114 | -- Remove packets |
|---|
| 115 | local rem = submit and luci.http.formvalue("remove") |
|---|
| 116 | if rem then |
|---|
| 117 | remove[rem], out, err = ipkg.remove(rem) |
|---|
| 118 | stdout[#stdout+1] = out |
|---|
| 119 | stderr[#stderr+1] = err |
|---|
| 120 | changes = true |
|---|
| 121 | end |
|---|
| 122 | |
|---|
| 123 | |
|---|
| 124 | -- Update all packets |
|---|
| 125 | local update = luci.http.formvalue("update") |
|---|
| 126 | if update then |
|---|
| 127 | update, out, err = ipkg.update() |
|---|
| 128 | stdout[#stdout+1] = out |
|---|
| 129 | stderr[#stderr+1] = err |
|---|
| 130 | end |
|---|
| 131 | |
|---|
| 132 | |
|---|
| 133 | -- Upgrade all packets |
|---|
| 134 | local upgrade = luci.http.formvalue("upgrade") |
|---|
| 135 | if upgrade then |
|---|
| 136 | upgrade, out, err = ipkg.upgrade() |
|---|
| 137 | stdout[#stdout+1] = out |
|---|
| 138 | stderr[#stderr+1] = err |
|---|
| 139 | end |
|---|
| 140 | |
|---|
| 141 | |
|---|
| 142 | -- List state |
|---|
| 143 | local no_lists = true |
|---|
| 144 | local old_lists = false |
|---|
| 145 | local tmp = nixio.fs.dir("/var/opkg-lists/") |
|---|
| 146 | if tmp then |
|---|
| 147 | for tmp in tmp do |
|---|
| 148 | no_lists = false |
|---|
| 149 | tmp = nixio.fs.stat("/var/opkg-lists/"..tmp) |
|---|
| 150 | if tmp and tmp.mtime < (os.time() - (24 * 60 * 60)) then |
|---|
| 151 | old_lists = true |
|---|
| 152 | break |
|---|
| 153 | end |
|---|
| 154 | end |
|---|
| 155 | end |
|---|
| 156 | |
|---|
| 157 | |
|---|
| 158 | luci.template.render("admin_system/packages", { |
|---|
| 159 | display = display, |
|---|
| 160 | letter = letter, |
|---|
| 161 | query = query, |
|---|
| 162 | install = install, |
|---|
| 163 | remove = remove, |
|---|
| 164 | update = update, |
|---|
| 165 | upgrade = upgrade, |
|---|
| 166 | no_lists = no_lists, |
|---|
| 167 | old_lists = old_lists, |
|---|
| 168 | stdout = table.concat(stdout, ""), |
|---|
| 169 | stderr = table.concat(stderr, "") |
|---|
| 170 | }) |
|---|
| 171 | |
|---|
| 172 | -- Remove index cache |
|---|
| 173 | if changes then |
|---|
| 174 | nixio.fs.unlink("/tmp/luci-indexcache") |
|---|
| 175 | end |
|---|
| 176 | end |
|---|
| 177 | |
|---|
| 178 | function action_flashops() |
|---|
| 179 | local sys = require "luci.sys" |
|---|
| 180 | local fs = require "luci.fs" |
|---|
| 181 | |
|---|
| 182 | local upgrade_avail = nixio.fs.access("/lib/upgrade/platform.sh") |
|---|
| 183 | local reset_avail = os.execute([[grep '"rootfs_data"' /proc/mtd >/dev/null 2>&1]]) == 0 |
|---|
| 184 | |
|---|
| 185 | local restore_cmd = "tar -xzC/ >/dev/null 2>&1" |
|---|
| 186 | local backup_cmd = "sysupgrade --create-backup - 2>/dev/null" |
|---|
| 187 | local image_tmp = "/tmp/firmware.img" |
|---|
| 188 | |
|---|
| 189 | local function image_supported() |
|---|
| 190 | -- XXX: yay... |
|---|
| 191 | return ( 0 == os.execute( |
|---|
| 192 | ". /lib/functions.sh; " .. |
|---|
| 193 | "include /lib/upgrade; " .. |
|---|
| 194 | "platform_check_image %q >/dev/null" |
|---|
| 195 | % image_tmp |
|---|
| 196 | ) ) |
|---|
| 197 | end |
|---|
| 198 | |
|---|
| 199 | local function image_checksum() |
|---|
| 200 | return (luci.sys.exec("md5sum %q" % image_tmp):match("^([^%s]+)")) |
|---|
| 201 | end |
|---|
| 202 | |
|---|
| 203 | local function storage_size() |
|---|
| 204 | local size = 0 |
|---|
| 205 | if nixio.fs.access("/proc/mtd") then |
|---|
| 206 | for l in io.lines("/proc/mtd") do |
|---|
| 207 | local d, s, e, n = l:match('^([^%s]+)%s+([^%s]+)%s+([^%s]+)%s+"([^%s]+)"') |
|---|
| 208 | if n == "linux" or n == "firmware" then |
|---|
| 209 | size = tonumber(s, 16) |
|---|
| 210 | break |
|---|
| 211 | end |
|---|
| 212 | end |
|---|
| 213 | elseif nixio.fs.access("/proc/partitions") then |
|---|
| 214 | for l in io.lines("/proc/partitions") do |
|---|
| 215 | local x, y, b, n = l:match('^%s*(%d+)%s+(%d+)%s+([^%s]+)%s+([^%s]+)') |
|---|
| 216 | if b and n and not n:match('[0-9]') then |
|---|
| 217 | size = tonumber(b) * 1024 |
|---|
| 218 | break |
|---|
| 219 | end |
|---|
| 220 | end |
|---|
| 221 | end |
|---|
| 222 | return size |
|---|
| 223 | end |
|---|
| 224 | |
|---|
| 225 | |
|---|
| 226 | local fp |
|---|
| 227 | luci.http.setfilehandler( |
|---|
| 228 | function(meta, chunk, eof) |
|---|
| 229 | if not fp then |
|---|
| 230 | if meta and meta.name == "image" then |
|---|
| 231 | fp = io.open(image_tmp, "w") |
|---|
| 232 | else |
|---|
| 233 | fp = io.popen(restore_cmd, "w") |
|---|
| 234 | end |
|---|
| 235 | end |
|---|
| 236 | if chunk then |
|---|
| 237 | fp:write(chunk) |
|---|
| 238 | end |
|---|
| 239 | if eof then |
|---|
| 240 | fp:close() |
|---|
| 241 | end |
|---|
| 242 | end |
|---|
| 243 | ) |
|---|
| 244 | |
|---|
| 245 | if luci.http.formvalue("backup") then |
|---|
| 246 | -- |
|---|
| 247 | -- Assemble file list, generate backup |
|---|
| 248 | -- |
|---|
| 249 | local reader = ltn12_popen(backup_cmd) |
|---|
| 250 | luci.http.header('Content-Disposition', 'attachment; filename="backup-%s-%s.tar.gz"' % { |
|---|
| 251 | luci.sys.hostname(), os.date("%Y-%m-%d")}) |
|---|
| 252 | luci.http.prepare_content("application/x-targz") |
|---|
| 253 | luci.ltn12.pump.all(reader, luci.http.write) |
|---|
| 254 | elseif luci.http.formvalue("restore") then |
|---|
| 255 | -- |
|---|
| 256 | -- Unpack received .tar.gz |
|---|
| 257 | -- |
|---|
| 258 | local upload = luci.http.formvalue("archive") |
|---|
| 259 | if upload and #upload > 0 then |
|---|
| 260 | luci.template.render("admin_system/applyreboot") |
|---|
| 261 | luci.sys.reboot() |
|---|
| 262 | end |
|---|
| 263 | elseif luci.http.formvalue("image") or luci.http.formvalue("step") then |
|---|
| 264 | -- |
|---|
| 265 | -- Initiate firmware flash |
|---|
| 266 | -- |
|---|
| 267 | local step = tonumber(luci.http.formvalue("step") or 1) |
|---|
| 268 | if step == 1 then |
|---|
| 269 | if image_supported() then |
|---|
| 270 | luci.template.render("admin_system/upgrade", { |
|---|
| 271 | checksum = image_checksum(), |
|---|
| 272 | storage = storage_size(), |
|---|
| 273 | size = nixio.fs.stat(image_tmp).size, |
|---|
| 274 | keep = (not not luci.http.formvalue("keep")) |
|---|
| 275 | }) |
|---|
| 276 | else |
|---|
| 277 | nixio.fs.unlink(image_tmp) |
|---|
| 278 | luci.template.render("admin_system/flashops", { |
|---|
| 279 | reset_avail = reset_avail, |
|---|
| 280 | upgrade_avail = upgrade_avail, |
|---|
| 281 | image_invalid = true |
|---|
| 282 | }) |
|---|
| 283 | end |
|---|
| 284 | -- |
|---|
| 285 | -- Start sysupgrade flash |
|---|
| 286 | -- |
|---|
| 287 | elseif step == 2 then |
|---|
| 288 | local keep = (luci.http.formvalue("keep") == "1") and "" or "-n" |
|---|
| 289 | luci.template.render("admin_system/applyreboot", { |
|---|
| 290 | title = luci.i18n.translate("Flashing..."), |
|---|
| 291 | msg = luci.i18n.translate("The system is flashing now.<br /> DO NOT POWER OFF THE DEVICE!<br /> Wait a few minutes until you try to reconnect. It might be necessary to renew the address of your computer to reach the device again, depending on your settings."), |
|---|
| 292 | addr = (#keep > 0) and "192.168.1.1" or nil |
|---|
| 293 | }) |
|---|
| 294 | fork_exec("killall dropbear uhttpd; sleep 1; /sbin/sysupgrade %s %q" %{ keep, image_tmp }) |
|---|
| 295 | end |
|---|
| 296 | elseif reset_avail and luci.http.formvalue("reset") then |
|---|
| 297 | -- |
|---|
| 298 | -- Reset system |
|---|
| 299 | -- |
|---|
| 300 | luci.template.render("admin_system/applyreboot", { |
|---|
| 301 | title = luci.i18n.translate("Erasing..."), |
|---|
| 302 | msg = luci.i18n.translate("The system is erasing the configuration partition now and will reboot itself when finished."), |
|---|
| 303 | addr = "192.168.1.1" |
|---|
| 304 | }) |
|---|
| 305 | fork_exec("killall dropbear uhttpd; sleep 1; mtd -r erase rootfs_data") |
|---|
| 306 | else |
|---|
| 307 | -- |
|---|
| 308 | -- Overview |
|---|
| 309 | -- |
|---|
| 310 | luci.template.render("admin_system/flashops", { |
|---|
| 311 | reset_avail = reset_avail, |
|---|
| 312 | upgrade_avail = upgrade_avail |
|---|
| 313 | }) |
|---|
| 314 | end |
|---|
| 315 | end |
|---|
| 316 | |
|---|
| 317 | function action_passwd() |
|---|
| 318 | local p1 = luci.http.formvalue("pwd1") |
|---|
| 319 | local p2 = luci.http.formvalue("pwd2") |
|---|
| 320 | local stat = nil |
|---|
| 321 | |
|---|
| 322 | if p1 or p2 then |
|---|
| 323 | if p1 == p2 then |
|---|
| 324 | stat = luci.sys.user.setpasswd("root", p1) |
|---|
| 325 | else |
|---|
| 326 | stat = 10 |
|---|
| 327 | end |
|---|
| 328 | end |
|---|
| 329 | |
|---|
| 330 | luci.template.render("admin_system/passwd", {stat=stat}) |
|---|
| 331 | end |
|---|
| 332 | |
|---|
| 333 | function action_reboot() |
|---|
| 334 | local reboot = luci.http.formvalue("reboot") |
|---|
| 335 | luci.template.render("admin_system/reboot", {reboot=reboot}) |
|---|
| 336 | if reboot then |
|---|
| 337 | luci.sys.reboot() |
|---|
| 338 | end |
|---|
| 339 | end |
|---|
| 340 | |
|---|
| 341 | function fork_exec(command) |
|---|
| 342 | local pid = nixio.fork() |
|---|
| 343 | if pid > 0 then |
|---|
| 344 | return |
|---|
| 345 | elseif pid == 0 then |
|---|
| 346 | -- change to root dir |
|---|
| 347 | nixio.chdir("/") |
|---|
| 348 | |
|---|
| 349 | -- patch stdin, out, err to /dev/null |
|---|
| 350 | local null = nixio.open("/dev/null", "w+") |
|---|
| 351 | if null then |
|---|
| 352 | nixio.dup(null, nixio.stderr) |
|---|
| 353 | nixio.dup(null, nixio.stdout) |
|---|
| 354 | nixio.dup(null, nixio.stdin) |
|---|
| 355 | if null:fileno() > 2 then |
|---|
| 356 | null:close() |
|---|
| 357 | end |
|---|
| 358 | end |
|---|
| 359 | |
|---|
| 360 | -- replace with target command |
|---|
| 361 | nixio.exec("/bin/sh", "-c", command) |
|---|
| 362 | end |
|---|
| 363 | end |
|---|
| 364 | |
|---|
| 365 | function ltn12_popen(command) |
|---|
| 366 | |
|---|
| 367 | local fdi, fdo = nixio.pipe() |
|---|
| 368 | local pid = nixio.fork() |
|---|
| 369 | |
|---|
| 370 | if pid > 0 then |
|---|
| 371 | fdo:close() |
|---|
| 372 | local close |
|---|
| 373 | return function() |
|---|
| 374 | local buffer = fdi:read(2048) |
|---|
| 375 | local wpid, stat = nixio.waitpid(pid, "nohang") |
|---|
| 376 | if not close and wpid and stat == "exited" then |
|---|
| 377 | close = true |
|---|
| 378 | end |
|---|
| 379 | |
|---|
| 380 | if buffer and #buffer > 0 then |
|---|
| 381 | return buffer |
|---|
| 382 | elseif close then |
|---|
| 383 | fdi:close() |
|---|
| 384 | return nil |
|---|
| 385 | end |
|---|
| 386 | end |
|---|
| 387 | elseif pid == 0 then |
|---|
| 388 | nixio.dup(fdo, nixio.stdout) |
|---|
| 389 | fdi:close() |
|---|
| 390 | fdo:close() |
|---|
| 391 | nixio.exec("/bin/sh", "-c", command) |
|---|
| 392 | end |
|---|
| 393 | end |
|---|