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