1-- 2-- SPDX-License-Identifier: BSD-2-Clause-FreeBSD 3-- 4-- Copyright (c) 2015 Pedro Souza <pedrosouza@freebsd.org> 5-- Copyright (C) 2018 Kyle Evans <kevans@FreeBSD.org> 6-- All rights reserved. 7-- 8-- Redistribution and use in source and binary forms, with or without 9-- modification, are permitted provided that the following conditions 10-- are met: 11-- 1. Redistributions of source code must retain the above copyright 12-- notice, this list of conditions and the following disclaimer. 13-- 2. Redistributions in binary form must reproduce the above copyright 14-- notice, this list of conditions and the following disclaimer in the 15-- documentation and/or other materials provided with the distribution. 16-- 17-- THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND 18-- ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 19-- IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 20-- ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE 21-- FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 22-- DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 23-- OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 24-- HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 25-- LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 26-- OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 27-- SUCH DAMAGE. 28-- 29-- $FreeBSD$ 30-- 31 32local hook = require("hook") 33 34local config = {} 35local modules = {} 36local carousel_choices = {} 37-- Which variables we changed 38local env_changed = {} 39-- Values to restore env to (nil to unset) 40local env_restore = {} 41 42local MSG_FAILEXEC = "Failed to exec '%s'" 43local MSG_FAILSETENV = "Failed to '%s' with value: %s" 44local MSG_FAILOPENCFG = "Failed to open config: '%s'" 45local MSG_FAILREADCFG = "Failed to read config: '%s'" 46local MSG_FAILPARSECFG = "Failed to parse config: '%s'" 47local MSG_FAILEXBEF = "Failed to execute '%s' before loading '%s'" 48local MSG_FAILEXMOD = "Failed to execute '%s'" 49local MSG_FAILEXAF = "Failed to execute '%s' after loading '%s'" 50local MSG_MALFORMED = "Malformed line (%d):\n\t'%s'" 51local MSG_DEFAULTKERNFAIL = "No kernel set, failed to load from module_path" 52local MSG_KERNFAIL = "Failed to load kernel '%s'" 53local MSG_KERNLOADING = "Loading kernel..." 54local MSG_MODLOADING = "Loading configured modules..." 55local MSG_MODLOADFAIL = "Could not load one or more modules!" 56 57local function restoreEnv() 58 -- Examine changed environment variables 59 for k, v in pairs(env_changed) do 60 local restore_value = env_restore[k] 61 if restore_value == nil then 62 -- This one doesn't need restored for some reason 63 goto continue 64 end 65 local current_value = loader.getenv(k) 66 if current_value ~= v then 67 -- This was overwritten by some action taken on the menu 68 -- most likely; we'll leave it be. 69 goto continue 70 end 71 restore_value = restore_value.value 72 if restore_value ~= nil then 73 loader.setenv(k, restore_value) 74 else 75 loader.unsetenv(k) 76 end 77 ::continue:: 78 end 79 80 env_changed = {} 81 env_restore = {} 82end 83 84local function setEnv(key, value) 85 -- Track the original value for this if we haven't already 86 if env_restore[key] == nil then 87 env_restore[key] = {value = loader.getenv(key)} 88 end 89 90 env_changed[key] = value 91 92 return loader.setenv(key, value) 93end 94 95-- name here is one of 'name', 'type', flags', 'before', 'after', or 'error.' 96-- These are set from lines in loader.conf(5): ${key}_${name}="${value}" where 97-- ${key} is a module name. 98local function setKey(key, name, value) 99 if modules[key] == nil then 100 modules[key] = {} 101 end 102 modules[key][name] = value 103end 104 105-- Escapes the named value for use as a literal in a replacement pattern. 106-- e.g. dhcp.host-name gets turned into dhcp%.host%-name to remove the special 107-- meaning. 108local function escapeName(name) 109 return name:gsub("([%p])", "%%%1") 110end 111 112local function processEnvVar(value) 113 for name in value:gmatch("${([^}]+)}") do 114 local replacement = loader.getenv(name) or "" 115 value = value:gsub("${" .. escapeName(name) .. "}", replacement) 116 end 117 for name in value:gmatch("$([%w%p]+)%s*") do 118 local replacement = loader.getenv(name) or "" 119 value = value:gsub("$" .. escapeName(name), replacement) 120 end 121 return value 122end 123 124local pattern_table = { 125 { 126 str = "^%s*(#.*)", 127 process = function(_, _) end, 128 }, 129 -- module_load="value" 130 { 131 str = "^%s*([%w_]+)_load%s*=%s*\"([%w%s%p]-)\"%s*(.*)", 132 process = function(k, v) 133 if modules[k] == nil then 134 modules[k] = {} 135 end 136 modules[k].load = v:upper() 137 end, 138 }, 139 -- module_name="value" 140 { 141 str = "^%s*([%w_]+)_name%s*=%s*\"([%w%s%p]-)\"%s*(.*)", 142 process = function(k, v) 143 setKey(k, "name", v) 144 end, 145 }, 146 -- module_type="value" 147 { 148 str = "^%s*([%w_]+)_type%s*=%s*\"([%w%s%p]-)\"%s*(.*)", 149 process = function(k, v) 150 setKey(k, "type", v) 151 end, 152 }, 153 -- module_flags="value" 154 { 155 str = "^%s*([%w_]+)_flags%s*=%s*\"([%w%s%p]-)\"%s*(.*)", 156 process = function(k, v) 157 setKey(k, "flags", v) 158 end, 159 }, 160 -- module_before="value" 161 { 162 str = "^%s*([%w_]+)_before%s*=%s*\"([%w%s%p]-)\"%s*(.*)", 163 process = function(k, v) 164 setKey(k, "before", v) 165 end, 166 }, 167 -- module_after="value" 168 { 169 str = "^%s*([%w_]+)_after%s*=%s*\"([%w%s%p]-)\"%s*(.*)", 170 process = function(k, v) 171 setKey(k, "after", v) 172 end, 173 }, 174 -- module_error="value" 175 { 176 str = "^%s*([%w_]+)_error%s*=%s*\"([%w%s%p]-)\"%s*(.*)", 177 process = function(k, v) 178 setKey(k, "error", v) 179 end, 180 }, 181 -- exec="command" 182 { 183 str = "^%s*exec%s*=%s*\"([%w%s%p]-)\"%s*(.*)", 184 process = function(k, _) 185 if cli_execute_unparsed(k) ~= 0 then 186 print(MSG_FAILEXEC:format(k)) 187 end 188 end, 189 }, 190 -- env_var="value" 191 { 192 str = "^%s*([%w%p]+)%s*=%s*\"([%w%s%p]-)\"%s*(.*)", 193 process = function(k, v) 194 if setEnv(k, processEnvVar(v)) ~= 0 then 195 print(MSG_FAILSETENV:format(k, v)) 196 end 197 end, 198 }, 199 -- env_var=num 200 { 201 str = "^%s*([%w%p]+)%s*=%s*(%d+)%s*(.*)", 202 process = function(k, v) 203 if setEnv(k, processEnvVar(v)) ~= 0 then 204 print(MSG_FAILSETENV:format(k, tostring(v))) 205 end 206 end, 207 }, 208} 209 210local function isValidComment(line) 211 if line ~= nil then 212 local s = line:match("^%s*#.*") 213 if s == nil then 214 s = line:match("^%s*$") 215 end 216 if s == nil then 217 return false 218 end 219 end 220 return true 221end 222 223local function loadModule(mod, silent) 224 local status = true 225 local pstatus 226 for k, v in pairs(mod) do 227 if v.load ~= nil and v.load:lower() == "yes" then 228 local str = "load " 229 if v.type ~= nil then 230 str = str .. "-t " .. v.type .. " " 231 end 232 if v.name ~= nil then 233 str = str .. v.name 234 else 235 str = str .. k 236 end 237 if v.flags ~= nil then 238 str = str .. " " .. v.flags 239 end 240 if v.before ~= nil then 241 pstatus = cli_execute_unparsed(v.before) == 0 242 if not pstatus and not silent then 243 print(MSG_FAILEXBEF:format(v.before, k)) 244 end 245 status = status and pstatus 246 end 247 248 if cli_execute_unparsed(str) ~= 0 then 249 if not silent then 250 print(MSG_FAILEXMOD:format(str)) 251 end 252 if v.error ~= nil then 253 cli_execute_unparsed(v.error) 254 end 255 status = false 256 end 257 258 if v.after ~= nil then 259 pstatus = cli_execute_unparsed(v.after) == 0 260 if not pstatus and not silent then 261 print(MSG_FAILEXAF:format(v.after, k)) 262 end 263 status = status and pstatus 264 end 265 266 end 267 end 268 269 return status 270end 271 272local function readConfFiles(loaded_files) 273 local f = loader.getenv("loader_conf_files") 274 if f ~= nil then 275 for name in f:gmatch("([%w%p]+)%s*") do 276 if loaded_files[name] ~= nil then 277 goto continue 278 end 279 280 local prefiles = loader.getenv("loader_conf_files") 281 282 print("Loading " .. name) 283 -- These may or may not exist, and that's ok. Do a 284 -- silent parse so that we complain on parse errors but 285 -- not for them simply not existing. 286 if not config.processFile(name, true) then 287 print(MSG_FAILPARSECFG:format(name)) 288 end 289 290 loaded_files[name] = true 291 local newfiles = loader.getenv("loader_conf_files") 292 if prefiles ~= newfiles then 293 readConfFiles(loaded_files) 294 end 295 ::continue:: 296 end 297 end 298end 299 300local function readFile(name, silent) 301 local f = io.open(name) 302 if f == nil then 303 if not silent then 304 print(MSG_FAILOPENCFG:format(name)) 305 end 306 return nil 307 end 308 309 local text, _ = io.read(f) 310 -- We might have read in the whole file, this won't be needed any more. 311 io.close(f) 312 313 if text == nil and not silent then 314 print(MSG_FAILREADCFG:format(name)) 315 end 316 return text 317end 318 319local function checkNextboot() 320 local nextboot_file = loader.getenv("nextboot_conf") 321 if nextboot_file == nil then 322 return 323 end 324 325 local text = readFile(nextboot_file, true) 326 if text == nil then 327 return 328 end 329 330 if text:match("^nextboot_enable=\"NO\"") ~= nil then 331 -- We're done; nextboot is not enabled 332 return 333 end 334 335 if not config.parse(text) then 336 print(MSG_FAILPARSECFG:format(nextboot_file)) 337 end 338 339 -- Attempt to rewrite the first line and only the first line of the 340 -- nextboot_file. We overwrite it with nextboot_enable="NO", then 341 -- check for that on load. 342 -- It's worth noting that this won't work on every filesystem, so we 343 -- won't do anything notable if we have any errors in this process. 344 local nfile = io.open(nextboot_file, 'w') 345 if nfile ~= nil then 346 -- We need the trailing space here to account for the extra 347 -- character taken up by the string nextboot_enable="YES" 348 -- Or new end quotation mark lands on the S, and we want to 349 -- rewrite the entirety of the first line. 350 io.write(nfile, "nextboot_enable=\"NO\" ") 351 io.close(nfile) 352 end 353end 354 355-- Module exports 356config.verbose = false 357 358-- The first item in every carousel is always the default item. 359function config.getCarouselIndex(id) 360 return carousel_choices[id] or 1 361end 362 363function config.setCarouselIndex(id, idx) 364 carousel_choices[id] = idx 365end 366 367-- Returns true if we processed the file successfully, false if we did not. 368-- If 'silent' is true, being unable to read the file is not considered a 369-- failure. 370function config.processFile(name, silent) 371 if silent == nil then 372 silent = false 373 end 374 375 local text = readFile(name, silent) 376 if text == nil then 377 return silent 378 end 379 380 return config.parse(text) 381end 382 383-- silent runs will not return false if we fail to open the file 384function config.parse(text) 385 local n = 1 386 local status = true 387 388 for line in text:gmatch("([^\n]+)") do 389 if line:match("^%s*$") == nil then 390 local found = false 391 392 for _, val in ipairs(pattern_table) do 393 local k, v, c = line:match(val.str) 394 if k ~= nil then 395 found = true 396 397 if isValidComment(c) then 398 val.process(k, v) 399 else 400 print(MSG_MALFORMED:format(n, 401 line)) 402 status = false 403 end 404 405 break 406 end 407 end 408 409 if not found then 410 print(MSG_MALFORMED:format(n, line)) 411 status = false 412 end 413 end 414 n = n + 1 415 end 416 417 return status 418end 419 420-- other_kernel is optionally the name of a kernel to load, if not the default 421-- or autoloaded default from the module_path 422function config.loadKernel(other_kernel) 423 local flags = loader.getenv("kernel_options") or "" 424 local kernel = other_kernel or loader.getenv("kernel") 425 426 local function tryLoad(names) 427 for name in names:gmatch("([^;]+)%s*;?") do 428 local r = loader.perform("load " .. name .. 429 " " .. flags) 430 if r == 0 then 431 return name 432 end 433 end 434 return nil 435 end 436 437 local function loadBootfile() 438 local bootfile = loader.getenv("bootfile") 439 440 -- append default kernel name 441 if bootfile == nil then 442 bootfile = "kernel" 443 else 444 bootfile = bootfile .. ";kernel" 445 end 446 447 return tryLoad(bootfile) 448 end 449 450 -- kernel not set, try load from default module_path 451 if kernel == nil then 452 local res = loadBootfile() 453 454 if res ~= nil then 455 -- Default kernel is loaded 456 config.kernel_loaded = nil 457 return true 458 else 459 print(MSG_DEFAULTKERNFAIL) 460 return false 461 end 462 else 463 -- Use our cached module_path, so we don't end up with multiple 464 -- automatically added kernel paths to our final module_path 465 local module_path = config.module_path 466 local res 467 468 if other_kernel ~= nil then 469 kernel = other_kernel 470 end 471 -- first try load kernel with module_path = /boot/${kernel} 472 -- then try load with module_path=${kernel} 473 local paths = {"/boot/" .. kernel, kernel} 474 475 for _, v in pairs(paths) do 476 loader.setenv("module_path", v) 477 res = loadBootfile() 478 479 -- succeeded, add path to module_path 480 if res ~= nil then 481 config.kernel_loaded = kernel 482 if module_path ~= nil then 483 loader.setenv("module_path", v .. ";" .. 484 module_path) 485 end 486 return true 487 end 488 end 489 490 -- failed to load with ${kernel} as a directory 491 -- try as a file 492 res = tryLoad(kernel) 493 if res ~= nil then 494 config.kernel_loaded = kernel 495 return true 496 else 497 print(MSG_KERNFAIL:format(kernel)) 498 return false 499 end 500 end 501end 502 503function config.selectKernel(kernel) 504 config.kernel_selected = kernel 505end 506 507function config.load(file, reloading) 508 if not file then 509 file = "/boot/defaults/loader.conf" 510 end 511 512 if not config.processFile(file) then 513 print(MSG_FAILPARSECFG:format(file)) 514 end 515 516 local loaded_files = {file = true} 517 readConfFiles(loaded_files) 518 519 checkNextboot() 520 521 -- Cache the provided module_path at load time for later use 522 config.module_path = loader.getenv("module_path") 523 local verbose = loader.getenv("verbose_loading") or "no" 524 config.verbose = verbose:lower() == "yes" 525 if not reloading then 526 hook.runAll("config.loaded") 527 end 528end 529 530-- Reload configuration 531function config.reload(file) 532 modules = {} 533 restoreEnv() 534 config.load(file, true) 535 hook.runAll("config.reloaded") 536end 537 538function config.loadelf() 539 local kernel = config.kernel_selected or config.kernel_loaded 540 local loaded 541 542 print(MSG_KERNLOADING) 543 loaded = config.loadKernel(kernel) 544 545 if not loaded then 546 return 547 end 548 549 print(MSG_MODLOADING) 550 if not loadModule(modules, not config.verbose) then 551 print(MSG_MODLOADFAIL) 552 end 553end 554 555hook.registerType("config.loaded") 556hook.registerType("config.reloaded") 557return config 558