xref: /freebsd/stand/lua/config.lua (revision 4f52dfbb8d6c4d446500c5b097e3806ec219fbd4)
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
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
253
254local function readFile(name, silent)
255	local f = io.open(name)
256	if f == nil then
257		if not silent then
258			print(MSG_FAILOPENCFG:format(name))
259		end
260		return nil
261	end
262
263	local text, _ = io.read(f)
264	-- We might have read in the whole file, this won't be needed any more.
265	io.close(f)
266
267	if text == nil and not silent then
268		print(MSG_FAILREADCFG:format(name))
269	end
270	return text
271end
272
273local function checkNextboot()
274	local nextboot_file = loader.getenv("nextboot_file")
275	if nextboot_file == nil then
276		return
277	end
278
279	local text = readFile(nextboot_file, true)
280	if text == nil then
281		return
282	end
283
284	if text:match("^nextboot_enable=\"NO\"") ~= nil then
285		-- We're done; nextboot is not enabled
286		return
287	end
288
289	if not config.parse(text) then
290		print(MSG_FAILPARSECFG:format(nextboot_file))
291	end
292
293	-- Attempt to rewrite the first line and only the first line of the
294	-- nextboot_file. We overwrite it with nextboot_enable="NO", then
295	-- check for that on load.
296	-- It's worth noting that this won't work on every filesystem, so we
297	-- won't do anything notable if we have any errors in this process.
298	local nfile = io.open(nextboot_file, 'w')
299	if nfile ~= nil then
300		-- We need the trailing space here to account for the extra
301		-- character taken up by the string nextboot_enable="YES"
302		-- Or new end quotation mark lands on the S, and we want to
303		-- rewrite the entirety of the first line.
304		io.write(nfile, "nextboot_enable=\"NO\" ")
305		io.close(nfile)
306	end
307end
308
309-- Module exports
310config.verbose = false
311
312-- The first item in every carousel is always the default item.
313function config.getCarouselIndex(id)
314	return carousel_choices[id] or 1
315end
316
317function config.setCarouselIndex(id, idx)
318	carousel_choices[id] = idx
319end
320
321-- Returns true if we processed the file successfully, false if we did not.
322-- If 'silent' is true, being unable to read the file is not considered a
323-- failure.
324function config.processFile(name, silent)
325	if silent == nil then
326		silent = false
327	end
328
329	local text = readFile(name, silent)
330	if text == nil then
331		return silent
332	end
333
334	return config.parse(text)
335end
336
337-- silent runs will not return false if we fail to open the file
338function config.parse(text)
339	local n = 1
340	local status = true
341
342	for line in text:gmatch("([^\n]+)") do
343		if line:match("^%s*$") == nil then
344			local found = false
345
346			for _, val in ipairs(pattern_table) do
347				local k, v, c = line:match(val.str)
348				if k ~= nil then
349					found = true
350
351					if isValidComment(c) then
352						val.process(k, v)
353					else
354						print(MSG_MALFORMED:format(n,
355						    line))
356						status = false
357					end
358
359					break
360				end
361			end
362
363			if not found then
364				print(MSG_MALFORMED:format(n, line))
365				status = false
366			end
367		end
368		n = n + 1
369	end
370
371	return status
372end
373
374-- other_kernel is optionally the name of a kernel to load, if not the default
375-- or autoloaded default from the module_path
376function config.loadKernel(other_kernel)
377	local flags = loader.getenv("kernel_options") or ""
378	local kernel = other_kernel or loader.getenv("kernel")
379
380	local function tryLoad(names)
381		for name in names:gmatch("([^;]+)%s*;?") do
382			local r = loader.perform("load " .. flags ..
383			    " " .. name)
384			if r == 0 then
385				return name
386			end
387		end
388		return nil
389	end
390
391	local function loadBootfile()
392		local bootfile = loader.getenv("bootfile")
393
394		-- append default kernel name
395		if bootfile == nil then
396			bootfile = "kernel"
397		else
398			bootfile = bootfile .. ";kernel"
399		end
400
401		return tryLoad(bootfile)
402	end
403
404	-- kernel not set, try load from default module_path
405	if kernel == nil then
406		local res = loadBootfile()
407
408		if res ~= nil then
409			-- Default kernel is loaded
410			config.kernel_loaded = nil
411			return true
412		else
413			print(MSG_DEFAULTKERNFAIL)
414			return false
415		end
416	else
417		-- Use our cached module_path, so we don't end up with multiple
418		-- automatically added kernel paths to our final module_path
419		local module_path = config.module_path
420		local res
421
422		if other_kernel ~= nil then
423			kernel = other_kernel
424		end
425		-- first try load kernel with module_path = /boot/${kernel}
426		-- then try load with module_path=${kernel}
427		local paths = {"/boot/" .. kernel, kernel}
428
429		for _, v in pairs(paths) do
430			loader.setenv("module_path", v)
431			res = loadBootfile()
432
433			-- succeeded, add path to module_path
434			if res ~= nil then
435				config.kernel_loaded = kernel
436				if module_path ~= nil then
437					loader.setenv("module_path", v .. ";" ..
438					    module_path)
439				end
440				return true
441			end
442		end
443
444		-- failed to load with ${kernel} as a directory
445		-- try as a file
446		res = tryLoad(kernel)
447		if res ~= nil then
448			config.kernel_loaded = kernel
449			return true
450		else
451			print(MSG_KERNFAIL:format(kernel))
452			return false
453		end
454	end
455end
456
457function config.selectKernel(kernel)
458	config.kernel_selected = kernel
459end
460
461function config.load(file)
462	if not file then
463		file = "/boot/defaults/loader.conf"
464	end
465
466	if not config.processFile(file) then
467		print(MSG_FAILPARSECFG:format(file))
468	end
469
470	local f = loader.getenv("loader_conf_files")
471	if f ~= nil then
472		for name in f:gmatch("([%w%p]+)%s*") do
473			-- These may or may not exist, and that's ok. Do a
474			-- silent parse so that we complain on parse errors but
475			-- not for them simply not existing.
476			if not config.processFile(name, true) then
477				print(MSG_FAILPARSECFG:format(name))
478			end
479		end
480	end
481
482	checkNextboot()
483
484	-- Cache the provided module_path at load time for later use
485	config.module_path = loader.getenv("module_path")
486	local verbose = loader.getenv("verbose_loading") or "no"
487	config.verbose = verbose:lower() == "yes"
488end
489
490-- Reload configuration
491function config.reload(file)
492	modules = {}
493	restoreEnv()
494	config.load(file)
495	hook.runAll("config.reloaded")
496end
497
498function config.loadelf()
499	local kernel = config.kernel_selected or config.kernel_loaded
500	local loaded
501
502	print(MSG_KERNLOADING)
503	loaded = config.loadKernel(kernel)
504
505	if not loaded then
506		return
507	end
508
509	print(MSG_MODLOADING)
510	if not loadModule(modules, not config.verbose) then
511		print(MSG_MODLOADFAIL)
512	end
513end
514
515hook.registerType("config.reloaded")
516return config
517