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