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