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