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