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_XENKERNFAIL = "Failed to load Xen kernel '%s'" 54local MSG_XENKERNLOADING = "Loading Xen kernel..." 55local MSG_KERNLOADING = "Loading kernel..." 56local MSG_MODLOADING = "Loading configured modules..." 57local MSG_MODLOADFAIL = "Could not load one or more modules!" 58 59local MODULEEXPR = '([%w-_]+)' 60 61local function restoreEnv() 62 -- Examine changed environment variables 63 for k, v in pairs(env_changed) do 64 local restore_value = env_restore[k] 65 if restore_value == nil then 66 -- This one doesn't need restored for some reason 67 goto continue 68 end 69 local current_value = loader.getenv(k) 70 if current_value ~= v then 71 -- This was overwritten by some action taken on the menu 72 -- most likely; we'll leave it be. 73 goto continue 74 end 75 restore_value = restore_value.value 76 if restore_value ~= nil then 77 loader.setenv(k, restore_value) 78 else 79 loader.unsetenv(k) 80 end 81 ::continue:: 82 end 83 84 env_changed = {} 85 env_restore = {} 86end 87 88local function setEnv(key, value) 89 -- Track the original value for this if we haven't already 90 if env_restore[key] == nil then 91 env_restore[key] = {value = loader.getenv(key)} 92 end 93 94 env_changed[key] = value 95 96 return loader.setenv(key, value) 97end 98 99-- name here is one of 'name', 'type', flags', 'before', 'after', or 'error.' 100-- These are set from lines in loader.conf(5): ${key}_${name}="${value}" where 101-- ${key} is a module name. 102local function setKey(key, name, value) 103 if modules[key] == nil then 104 modules[key] = {} 105 end 106 modules[key][name] = value 107end 108 109-- Escapes the named value for use as a literal in a replacement pattern. 110-- e.g. dhcp.host-name gets turned into dhcp%.host%-name to remove the special 111-- meaning. 112local function escapeName(name) 113 return name:gsub("([%p])", "%%%1") 114end 115 116local function processEnvVar(value) 117 for name in value:gmatch("${([^}]+)}") do 118 local replacement = loader.getenv(name) or "" 119 value = value:gsub("${" .. escapeName(name) .. "}", replacement) 120 end 121 for name in value:gmatch("$([%w%p]+)%s*") do 122 local replacement = loader.getenv(name) or "" 123 value = value:gsub("$" .. escapeName(name), replacement) 124 end 125 return value 126end 127 128-- str in this table is a regex pattern. It will automatically be anchored to 129-- the beginning of a line and any preceding whitespace will be skipped. The 130-- pattern should have no more than two captures patterns, which correspond to 131-- the two parameters (usually 'key' and 'value') that are passed to the 132-- process function. All trailing characters will be validated. 133-- 134-- We have two special entries in this table: the first is the first entry, 135-- a full-line comment. The second is for 'exec' handling. Both have a single 136-- capture group, but the difference is that the full-line comment pattern will 137-- match the entire line. This does not run afoul of the later end of line 138-- validation that we'll do after a match. However, the 'exec' pattern will. 139-- We document the exceptions with a special 'groups' index that indicates 140-- the number of capture groups, if not two. We'll use this later to do 141-- validation on the proper entry. 142local pattern_table = { 143 { 144 str = "(#.*)", 145 process = function(_, _) end, 146 groups = 1, 147 }, 148 -- module_load="value" 149 { 150 str = MODULEEXPR .. "_load%s*=%s*\"([%w%s%p]-)\"", 151 process = function(k, v) 152 if modules[k] == nil then 153 modules[k] = {} 154 end 155 modules[k].load = v:upper() 156 end, 157 }, 158 -- module_name="value" 159 { 160 str = MODULEEXPR .. "_name%s*=%s*\"([%w%s%p]-)\"", 161 process = function(k, v) 162 setKey(k, "name", v) 163 end, 164 }, 165 -- module_type="value" 166 { 167 str = MODULEEXPR .. "_type%s*=%s*\"([%w%s%p]-)\"", 168 process = function(k, v) 169 setKey(k, "type", v) 170 end, 171 }, 172 -- module_flags="value" 173 { 174 str = MODULEEXPR .. "_flags%s*=%s*\"([%w%s%p]-)\"", 175 process = function(k, v) 176 setKey(k, "flags", v) 177 end, 178 }, 179 -- module_before="value" 180 { 181 str = MODULEEXPR .. "_before%s*=%s*\"([%w%s%p]-)\"", 182 process = function(k, v) 183 setKey(k, "before", v) 184 end, 185 }, 186 -- module_after="value" 187 { 188 str = MODULEEXPR .. "_after%s*=%s*\"([%w%s%p]-)\"", 189 process = function(k, v) 190 setKey(k, "after", v) 191 end, 192 }, 193 -- module_error="value" 194 { 195 str = MODULEEXPR .. "_error%s*=%s*\"([%w%s%p]-)\"", 196 process = function(k, v) 197 setKey(k, "error", v) 198 end, 199 }, 200 -- exec="command" 201 { 202 str = "exec%s*=%s*\"([%w%s%p]-)\"", 203 process = function(k, _) 204 if cli_execute_unparsed(k) ~= 0 then 205 print(MSG_FAILEXEC:format(k)) 206 end 207 end, 208 groups = 1, 209 }, 210 -- env_var="value" 211 { 212 str = "([%w%p]+)%s*=%s*\"([%w%s%p]-)\"", 213 process = function(k, v) 214 if setEnv(k, processEnvVar(v)) ~= 0 then 215 print(MSG_FAILSETENV:format(k, v)) 216 end 217 end, 218 }, 219 -- env_var=num 220 { 221 str = "([%w%p]+)%s*=%s*(-?%d+)", 222 process = function(k, v) 223 if setEnv(k, processEnvVar(v)) ~= 0 then 224 print(MSG_FAILSETENV:format(k, tostring(v))) 225 end 226 end, 227 }, 228} 229 230local function isValidComment(line) 231 if line ~= nil then 232 local s = line:match("^%s*#.*") 233 if s == nil then 234 s = line:match("^%s*$") 235 end 236 if s == nil then 237 return false 238 end 239 end 240 return true 241end 242 243local function loadModule(mod, silent) 244 local status = true 245 local pstatus 246 for k, v in pairs(mod) do 247 if v.load ~= nil and v.load:lower() == "yes" then 248 local str = "load " 249 if v.type ~= nil then 250 str = str .. "-t " .. v.type .. " " 251 end 252 if v.name ~= nil then 253 str = str .. v.name 254 else 255 str = str .. k 256 end 257 if v.flags ~= nil then 258 str = str .. " " .. v.flags 259 end 260 if v.before ~= nil then 261 pstatus = cli_execute_unparsed(v.before) == 0 262 if not pstatus and not silent then 263 print(MSG_FAILEXBEF:format(v.before, k)) 264 end 265 status = status and pstatus 266 end 267 268 if cli_execute_unparsed(str) ~= 0 then 269 if not silent then 270 print(MSG_FAILEXMOD:format(str)) 271 end 272 if v.error ~= nil then 273 cli_execute_unparsed(v.error) 274 end 275 status = false 276 end 277 278 if v.after ~= nil then 279 pstatus = cli_execute_unparsed(v.after) == 0 280 if not pstatus and not silent then 281 print(MSG_FAILEXAF:format(v.after, k)) 282 end 283 status = status and pstatus 284 end 285 286 end 287 end 288 289 return status 290end 291 292local function readConfFiles(loaded_files) 293 local f = loader.getenv("loader_conf_files") 294 if f ~= nil then 295 for name in f:gmatch("([%w%p]+)%s*") do 296 if loaded_files[name] ~= nil then 297 goto continue 298 end 299 300 local prefiles = loader.getenv("loader_conf_files") 301 302 print("Loading " .. name) 303 -- These may or may not exist, and that's ok. Do a 304 -- silent parse so that we complain on parse errors but 305 -- not for them simply not existing. 306 if not config.processFile(name, true) then 307 print(MSG_FAILPARSECFG:format(name)) 308 end 309 310 loaded_files[name] = true 311 local newfiles = loader.getenv("loader_conf_files") 312 if prefiles ~= newfiles then 313 readConfFiles(loaded_files) 314 end 315 ::continue:: 316 end 317 end 318end 319 320local function readFile(name, silent) 321 local f = io.open(name) 322 if f == nil then 323 if not silent then 324 print(MSG_FAILOPENCFG:format(name)) 325 end 326 return nil 327 end 328 329 local text, _ = io.read(f) 330 -- We might have read in the whole file, this won't be needed any more. 331 io.close(f) 332 333 if text == nil and not silent then 334 print(MSG_FAILREADCFG:format(name)) 335 end 336 return text 337end 338 339local function checkNextboot() 340 local nextboot_file = loader.getenv("nextboot_conf") 341 if nextboot_file == nil then 342 return 343 end 344 345 local text = readFile(nextboot_file, true) 346 if text == nil then 347 return 348 end 349 350 if text:match("^nextboot_enable=\"NO\"") ~= nil then 351 -- We're done; nextboot is not enabled 352 return 353 end 354 355 if not config.parse(text) then 356 print(MSG_FAILPARSECFG:format(nextboot_file)) 357 end 358 359 -- Attempt to rewrite the first line and only the first line of the 360 -- nextboot_file. We overwrite it with nextboot_enable="NO", then 361 -- check for that on load. 362 -- It's worth noting that this won't work on every filesystem, so we 363 -- won't do anything notable if we have any errors in this process. 364 local nfile = io.open(nextboot_file, 'w') 365 if nfile ~= nil then 366 -- We need the trailing space here to account for the extra 367 -- character taken up by the string nextboot_enable="YES" 368 -- Or new end quotation mark lands on the S, and we want to 369 -- rewrite the entirety of the first line. 370 io.write(nfile, "nextboot_enable=\"NO\" ") 371 io.close(nfile) 372 end 373end 374 375-- Module exports 376config.verbose = false 377 378-- The first item in every carousel is always the default item. 379function config.getCarouselIndex(id) 380 return carousel_choices[id] or 1 381end 382 383function config.setCarouselIndex(id, idx) 384 carousel_choices[id] = idx 385end 386 387-- Returns true if we processed the file successfully, false if we did not. 388-- If 'silent' is true, being unable to read the file is not considered a 389-- failure. 390function config.processFile(name, silent) 391 if silent == nil then 392 silent = false 393 end 394 395 local text = readFile(name, silent) 396 if text == nil then 397 return silent 398 end 399 400 return config.parse(text) 401end 402 403-- silent runs will not return false if we fail to open the file 404function config.parse(text) 405 local n = 1 406 local status = true 407 408 for line in text:gmatch("([^\n]+)") do 409 if line:match("^%s*$") == nil then 410 for _, val in ipairs(pattern_table) do 411 local pattern = '^%s*' .. val.str .. '%s*(.*)'; 412 local cgroups = val.groups or 2 413 local k, v, c = line:match(pattern) 414 if k ~= nil then 415 -- Offset by one, drats 416 if cgroups == 1 then 417 c = v 418 v = nil 419 end 420 421 if isValidComment(c) then 422 val.process(k, v) 423 goto nextline 424 end 425 426 break 427 end 428 end 429 430 print(MSG_MALFORMED:format(n, line)) 431 status = false 432 end 433 ::nextline:: 434 n = n + 1 435 end 436 437 return status 438end 439 440-- other_kernel is optionally the name of a kernel to load, if not the default 441-- or autoloaded default from the module_path 442function config.loadKernel(other_kernel) 443 local flags = loader.getenv("kernel_options") or "" 444 local kernel = other_kernel or loader.getenv("kernel") 445 446 local function tryLoad(names) 447 for name in names:gmatch("([^;]+)%s*;?") do 448 local r = loader.perform("load " .. name .. 449 " " .. flags) 450 if r == 0 then 451 return name 452 end 453 end 454 return nil 455 end 456 457 local function loadBootfile() 458 local bootfile = loader.getenv("bootfile") 459 460 -- append default kernel name 461 if bootfile == nil then 462 bootfile = "kernel" 463 else 464 bootfile = bootfile .. ";kernel" 465 end 466 467 return tryLoad(bootfile) 468 end 469 470 -- kernel not set, try load from default module_path 471 if kernel == nil then 472 local res = loadBootfile() 473 474 if res ~= nil then 475 -- Default kernel is loaded 476 config.kernel_loaded = nil 477 return true 478 else 479 print(MSG_DEFAULTKERNFAIL) 480 return false 481 end 482 else 483 -- Use our cached module_path, so we don't end up with multiple 484 -- automatically added kernel paths to our final module_path 485 local module_path = config.module_path 486 local res 487 488 if other_kernel ~= nil then 489 kernel = other_kernel 490 end 491 -- first try load kernel with module_path = /boot/${kernel} 492 -- then try load with module_path=${kernel} 493 local paths = {"/boot/" .. kernel, kernel} 494 495 for _, v in pairs(paths) do 496 loader.setenv("module_path", v) 497 res = loadBootfile() 498 499 -- succeeded, add path to module_path 500 if res ~= nil then 501 config.kernel_loaded = kernel 502 if module_path ~= nil then 503 loader.setenv("module_path", v .. ";" .. 504 module_path) 505 end 506 return true 507 end 508 end 509 510 -- failed to load with ${kernel} as a directory 511 -- try as a file 512 res = tryLoad(kernel) 513 if res ~= nil then 514 config.kernel_loaded = kernel 515 return true 516 else 517 print(MSG_KERNFAIL:format(kernel)) 518 return false 519 end 520 end 521end 522 523function config.selectKernel(kernel) 524 config.kernel_selected = kernel 525end 526 527function config.load(file, reloading) 528 if not file then 529 file = "/boot/defaults/loader.conf" 530 end 531 532 if not config.processFile(file) then 533 print(MSG_FAILPARSECFG:format(file)) 534 end 535 536 local loaded_files = {file = true} 537 readConfFiles(loaded_files) 538 539 checkNextboot() 540 541 -- Cache the provided module_path at load time for later use 542 config.module_path = loader.getenv("module_path") 543 local verbose = loader.getenv("verbose_loading") or "no" 544 config.verbose = verbose:lower() == "yes" 545 if not reloading then 546 hook.runAll("config.loaded") 547 end 548end 549 550-- Reload configuration 551function config.reload(file) 552 modules = {} 553 restoreEnv() 554 config.load(file, true) 555 hook.runAll("config.reloaded") 556end 557 558function config.loadelf() 559 local xen_kernel = loader.getenv('xen_kernel') 560 local kernel = config.kernel_selected or config.kernel_loaded 561 local loaded 562 563 if xen_kernel ~= nil then 564 print(MSG_XENKERNLOADING) 565 if cli_execute_unparsed('load ' .. xen_kernel) ~= 0 then 566 print(MSG_XENKERNFAIL:format(xen_kernel)) 567 return 568 end 569 end 570 print(MSG_KERNLOADING) 571 loaded = config.loadKernel(kernel) 572 573 if not loaded then 574 return 575 end 576 577 print(MSG_MODLOADING) 578 if not loadModule(modules, not config.verbose) then 579 print(MSG_MODLOADFAIL) 580 end 581end 582 583hook.registerType("config.loaded") 584hook.registerType("config.reloaded") 585return config 586