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