xref: /freebsd-12.1/stand/lua/config.lua (revision f0a2d31a)
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(k, v)
232	-- Track the original value for this if we haven't already
233	if config.env_restore[k] == nil then
234		config.env_restore[k] = {value = loader.getenv(k)}
235	end
236
237	config.env_changed[k] = v
238
239	return loader.setenv(k, v)
240end
241
242function config.setKey(k, n, v)
243	if modules[k] == nil then
244		modules[k] = {}
245	end
246	modules[k][n] = v
247end
248
249function config.lsModules()
250	print("== Listing modules")
251	for k, v in pairs(modules) do
252		print(k, v.load)
253	end
254	print("== List of modules ended")
255end
256
257
258function config.isValidComment(c)
259	if c ~= nil then
260		local s = c:match("^%s*#.*")
261		if s == nil then
262			s = c:match("^%s*$")
263		end
264		if s == nil then
265			return false
266		end
267	end
268	return true
269end
270
271function config.loadmod(mod, silent)
272	local status = true
273	for k, v in pairs(mod) do
274		if v.load == "YES" then
275			local str = "load "
276			if v.flags ~= nil then
277				str = str .. v.flags .. " "
278			end
279			if v.type ~= nil then
280				str = str .. "-t " .. v.type .. " "
281			end
282			if v.name ~= nil then
283				str = str .. v.name
284			else
285				str = str .. k
286			end
287
288			if v.before ~= nil then
289				if loader.perform(v.before) ~= 0 then
290					if not silent then
291						print("Failed to execute '" ..
292						    v.before ..
293						    "' before loading '" .. k ..
294						    "'")
295					end
296					status = false
297				end
298			end
299
300			if loader.perform(str) ~= 0 then
301				if not silent then
302					print("Failed to execute '" .. str ..
303					    "'")
304				end
305				if v.error ~= nil then
306					loader.perform(v.error)
307				end
308				status = false
309			end
310
311			if v.after ~= nil then
312				if loader.perform(v.after) ~= 0 then
313					if not silent then
314						print("Failed to execute '" ..
315						    v.after ..
316						    "' after loading '" .. k ..
317						    "'")
318					end
319					status = false
320				end
321			end
322
323--		else
324--			if not silent then
325--				print("Skipping module '". . k .. "'")
326--			end
327		end
328	end
329
330	return status
331end
332
333function config.processFile(name, silent)
334	if silent == nil then
335		silent = false
336	end
337
338	local text = readFile(name, silent)
339	if text == nil then
340		return not silent
341	end
342
343	return config.parse(text)
344end
345
346-- silent runs will not return false if we fail to open the file
347-- check_and_halt, if it's set, will be executed on the full text of the config
348-- file. If it returns false, we are to halt immediately.
349function config.parse(text)
350	local n = 1
351	local status = true
352
353	for line in text:gmatch("([^\n]+)") do
354		if line:match("^%s*$") == nil then
355			local found = false
356
357			for _, val in ipairs(pattern_table) do
358				local k, v, c = line:match(val.str)
359				if k ~= nil then
360					found = true
361
362					if config.isValidComment(c) then
363						val.process(k, v)
364					else
365						print("Malformed line (" .. n ..
366						    "):\n\t'" .. line .. "'")
367						status = false
368					end
369
370					break
371				end
372			end
373
374			if not found then
375				print("Malformed line (" .. n .. "):\n\t'" ..
376				    line .. "'")
377				status = false
378			end
379		end
380		n = n + 1
381	end
382
383	return status
384end
385
386-- other_kernel is optionally the name of a kernel to load, if not the default
387-- or autoloaded default from the module_path
388function config.loadKernel(other_kernel)
389	local flags = loader.getenv("kernel_options") or ""
390	local kernel = other_kernel or loader.getenv("kernel")
391
392	local function tryLoad(names)
393		for name in names:gmatch("([^;]+)%s*;?") do
394			local r = loader.perform("load " .. flags ..
395			    " " .. name)
396			if r == 0 then
397				return name
398			end
399		end
400		return nil
401	end
402
403	local function loadBootfile()
404		local bootfile = loader.getenv("bootfile")
405
406		-- append default kernel name
407		if bootfile == nil then
408			bootfile = "kernel"
409		else
410			bootfile = bootfile .. ";kernel"
411		end
412
413		return tryLoad(bootfile)
414	end
415
416	-- kernel not set, try load from default module_path
417	if kernel == nil then
418		local res = loadBootfile()
419
420		if res ~= nil then
421			-- Default kernel is loaded
422			config.kernel_loaded = nil
423			return true
424		else
425			print("No kernel set, failed to load from module_path")
426			return false
427		end
428	else
429		-- Use our cached module_path, so we don't end up with multiple
430		-- automatically added kernel paths to our final module_path
431		local module_path = config.module_path
432		local res
433
434		if other_kernel ~= nil then
435			kernel = other_kernel
436		end
437		-- first try load kernel with module_path = /boot/${kernel}
438		-- then try load with module_path=${kernel}
439		local paths = {"/boot/" .. kernel, kernel}
440
441		for _, v in pairs(paths) do
442			loader.setenv("module_path", v)
443			res = loadBootfile()
444
445			-- succeeded, add path to module_path
446			if res ~= nil then
447				config.kernel_loaded = kernel
448				if module_path ~= nil then
449					loader.setenv("module_path", v .. ";" ..
450					    module_path)
451				end
452				return true
453			end
454		end
455
456		-- failed to load with ${kernel} as a directory
457		-- try as a file
458		res = tryLoad(kernel)
459		if res ~= nil then
460			config.kernel_loaded = kernel
461			return true
462		else
463			print("Failed to load kernel '" .. kernel .. "'")
464			return false
465		end
466	end
467end
468
469function config.selectKernel(kernel)
470	config.kernel_selected = kernel
471end
472
473function config.load(file)
474	if not file then
475		file = "/boot/defaults/loader.conf"
476	end
477
478	if not config.processFile(file) then
479		print("Failed to parse configuration: '" .. file .. "'")
480	end
481
482	local f = loader.getenv("loader_conf_files")
483	if f ~= nil then
484		for name in f:gmatch("([%w%p]+)%s*") do
485			-- These may or may not exist, and that's ok. Do a
486			-- silent parse so that we complain on parse errors but
487			-- not for them simply not existing.
488			if not config.processFile(name, true) then
489				print("Failed to parse configuration: '" ..
490				    name .. "'")
491			end
492		end
493	end
494
495	checkNextboot()
496
497	-- Cache the provided module_path at load time for later use
498	config.module_path = loader.getenv("module_path")
499end
500
501-- Reload configuration
502function config.reload(file)
503	modules = {}
504	config.restoreEnv()
505	config.load(file)
506end
507
508function config.loadelf()
509	local kernel = config.kernel_selected or config.kernel_loaded
510	local loaded
511
512	print("Loading kernel...")
513	loaded = config.loadKernel(kernel)
514
515	if not loaded then
516		print("Failed to load any kernel")
517		return
518	end
519
520	print("Loading configured modules...")
521	if not config.loadmod(modules) then
522		print("Could not load one or more modules!")
523	end
524end
525
526return config
527