xref: /freebsd/stand/lua/config.lua (revision 780fb4a2)
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
105-- Escapes the named value for use as a literal in a replacement pattern.
106-- e.g. dhcp.host-name gets turned into dhcp%.host%-name to remove the special
107-- meaning.
108local function escapeName(name)
109	return name:gsub("([%p])", "%%%1")
110end
111
112local function processEnvVar(value)
113	for name in value:gmatch("${([^}]+)}") do
114		local replacement = loader.getenv(name) or ""
115		value = value:gsub("${" .. escapeName(name) .. "}", replacement)
116	end
117	for name in value:gmatch("$([%w%p]+)%s*") do
118		local replacement = loader.getenv(name) or ""
119		value = value:gsub("$" .. escapeName(name), replacement)
120	end
121	return value
122end
123
124local pattern_table = {
125	{
126		str = "^%s*(#.*)",
127		process = function(_, _)  end,
128	},
129	--  module_load="value"
130	{
131		str = "^%s*([%w_]+)_load%s*=%s*\"([%w%s%p]-)\"%s*(.*)",
132		process = function(k, v)
133			if modules[k] == nil then
134				modules[k] = {}
135			end
136			modules[k].load = v:upper()
137		end,
138	},
139	--  module_name="value"
140	{
141		str = "^%s*([%w_]+)_name%s*=%s*\"([%w%s%p]-)\"%s*(.*)",
142		process = function(k, v)
143			setKey(k, "name", v)
144		end,
145	},
146	--  module_type="value"
147	{
148		str = "^%s*([%w_]+)_type%s*=%s*\"([%w%s%p]-)\"%s*(.*)",
149		process = function(k, v)
150			setKey(k, "type", v)
151		end,
152	},
153	--  module_flags="value"
154	{
155		str = "^%s*([%w_]+)_flags%s*=%s*\"([%w%s%p]-)\"%s*(.*)",
156		process = function(k, v)
157			setKey(k, "flags", v)
158		end,
159	},
160	--  module_before="value"
161	{
162		str = "^%s*([%w_]+)_before%s*=%s*\"([%w%s%p]-)\"%s*(.*)",
163		process = function(k, v)
164			setKey(k, "before", v)
165		end,
166	},
167	--  module_after="value"
168	{
169		str = "^%s*([%w_]+)_after%s*=%s*\"([%w%s%p]-)\"%s*(.*)",
170		process = function(k, v)
171			setKey(k, "after", v)
172		end,
173	},
174	--  module_error="value"
175	{
176		str = "^%s*([%w_]+)_error%s*=%s*\"([%w%s%p]-)\"%s*(.*)",
177		process = function(k, v)
178			setKey(k, "error", v)
179		end,
180	},
181	--  exec="command"
182	{
183		str = "^%s*exec%s*=%s*\"([%w%s%p]-)\"%s*(.*)",
184		process = function(k, _)
185			if cli_execute_unparsed(k) ~= 0 then
186				print(MSG_FAILEXEC:format(k))
187			end
188		end,
189	},
190	--  env_var="value"
191	{
192		str = "^%s*([%w%p]+)%s*=%s*\"([%w%s%p]-)\"%s*(.*)",
193		process = function(k, v)
194			if setEnv(k, processEnvVar(v)) ~= 0 then
195				print(MSG_FAILSETENV:format(k, v))
196			end
197		end,
198	},
199	--  env_var=num
200	{
201		str = "^%s*([%w%p]+)%s*=%s*(%d+)%s*(.*)",
202		process = function(k, v)
203			if setEnv(k, processEnvVar(v)) ~= 0 then
204				print(MSG_FAILSETENV:format(k, tostring(v)))
205			end
206		end,
207	},
208}
209
210local function isValidComment(line)
211	if line ~= nil then
212		local s = line:match("^%s*#.*")
213		if s == nil then
214			s = line:match("^%s*$")
215		end
216		if s == nil then
217			return false
218		end
219	end
220	return true
221end
222
223local function loadModule(mod, silent)
224	local status = true
225	local pstatus
226	for k, v in pairs(mod) do
227		if v.load ~= nil and v.load:lower() == "yes" then
228			local str = "load "
229			if v.type ~= nil then
230				str = str .. "-t " .. v.type .. " "
231			end
232			if v.name ~= nil then
233				str = str .. v.name
234			else
235				str = str .. k
236			end
237			if v.flags ~= nil then
238				str = str .. " " .. v.flags
239			end
240			if v.before ~= nil then
241				pstatus = cli_execute_unparsed(v.before) == 0
242				if not pstatus and not silent then
243					print(MSG_FAILEXBEF:format(v.before, k))
244				end
245				status = status and pstatus
246			end
247
248			if cli_execute_unparsed(str) ~= 0 then
249				if not silent then
250					print(MSG_FAILEXMOD:format(str))
251				end
252				if v.error ~= nil then
253					cli_execute_unparsed(v.error)
254				end
255				status = false
256			end
257
258			if v.after ~= nil then
259				pstatus = cli_execute_unparsed(v.after) == 0
260				if not pstatus and not silent then
261					print(MSG_FAILEXAF:format(v.after, k))
262				end
263				status = status and pstatus
264			end
265
266		end
267	end
268
269	return status
270end
271
272local function readConfFiles(loaded_files)
273	local f = loader.getenv("loader_conf_files")
274	if f ~= nil then
275		for name in f:gmatch("([%w%p]+)%s*") do
276			if loaded_files[name] ~= nil then
277				goto continue
278			end
279
280			local prefiles = loader.getenv("loader_conf_files")
281
282			print("Loading " .. name)
283			-- These may or may not exist, and that's ok. Do a
284			-- silent parse so that we complain on parse errors but
285			-- not for them simply not existing.
286			if not config.processFile(name, true) then
287				print(MSG_FAILPARSECFG:format(name))
288			end
289
290			loaded_files[name] = true
291			local newfiles = loader.getenv("loader_conf_files")
292			if prefiles ~= newfiles then
293				readConfFiles(loaded_files)
294			end
295			::continue::
296		end
297	end
298end
299
300local function readFile(name, silent)
301	local f = io.open(name)
302	if f == nil then
303		if not silent then
304			print(MSG_FAILOPENCFG:format(name))
305		end
306		return nil
307	end
308
309	local text, _ = io.read(f)
310	-- We might have read in the whole file, this won't be needed any more.
311	io.close(f)
312
313	if text == nil and not silent then
314		print(MSG_FAILREADCFG:format(name))
315	end
316	return text
317end
318
319local function checkNextboot()
320	local nextboot_file = loader.getenv("nextboot_file")
321	if nextboot_file == nil then
322		return
323	end
324
325	local text = readFile(nextboot_file, true)
326	if text == nil then
327		return
328	end
329
330	if text:match("^nextboot_enable=\"NO\"") ~= nil then
331		-- We're done; nextboot is not enabled
332		return
333	end
334
335	if not config.parse(text) then
336		print(MSG_FAILPARSECFG:format(nextboot_file))
337	end
338
339	-- Attempt to rewrite the first line and only the first line of the
340	-- nextboot_file. We overwrite it with nextboot_enable="NO", then
341	-- check for that on load.
342	-- It's worth noting that this won't work on every filesystem, so we
343	-- won't do anything notable if we have any errors in this process.
344	local nfile = io.open(nextboot_file, 'w')
345	if nfile ~= nil then
346		-- We need the trailing space here to account for the extra
347		-- character taken up by the string nextboot_enable="YES"
348		-- Or new end quotation mark lands on the S, and we want to
349		-- rewrite the entirety of the first line.
350		io.write(nfile, "nextboot_enable=\"NO\" ")
351		io.close(nfile)
352	end
353end
354
355-- Module exports
356config.verbose = false
357
358-- The first item in every carousel is always the default item.
359function config.getCarouselIndex(id)
360	return carousel_choices[id] or 1
361end
362
363function config.setCarouselIndex(id, idx)
364	carousel_choices[id] = idx
365end
366
367-- Returns true if we processed the file successfully, false if we did not.
368-- If 'silent' is true, being unable to read the file is not considered a
369-- failure.
370function config.processFile(name, silent)
371	if silent == nil then
372		silent = false
373	end
374
375	local text = readFile(name, silent)
376	if text == nil then
377		return silent
378	end
379
380	return config.parse(text)
381end
382
383-- silent runs will not return false if we fail to open the file
384function config.parse(text)
385	local n = 1
386	local status = true
387
388	for line in text:gmatch("([^\n]+)") do
389		if line:match("^%s*$") == nil then
390			local found = false
391
392			for _, val in ipairs(pattern_table) do
393				local k, v, c = line:match(val.str)
394				if k ~= nil then
395					found = true
396
397					if isValidComment(c) then
398						val.process(k, v)
399					else
400						print(MSG_MALFORMED:format(n,
401						    line))
402						status = false
403					end
404
405					break
406				end
407			end
408
409			if not found then
410				print(MSG_MALFORMED:format(n, line))
411				status = false
412			end
413		end
414		n = n + 1
415	end
416
417	return status
418end
419
420-- other_kernel is optionally the name of a kernel to load, if not the default
421-- or autoloaded default from the module_path
422function config.loadKernel(other_kernel)
423	local flags = loader.getenv("kernel_options") or ""
424	local kernel = other_kernel or loader.getenv("kernel")
425
426	local function tryLoad(names)
427		for name in names:gmatch("([^;]+)%s*;?") do
428			local r = loader.perform("load " .. name ..
429			     " " .. flags)
430			if r == 0 then
431				return name
432			end
433		end
434		return nil
435	end
436
437	local function loadBootfile()
438		local bootfile = loader.getenv("bootfile")
439
440		-- append default kernel name
441		if bootfile == nil then
442			bootfile = "kernel"
443		else
444			bootfile = bootfile .. ";kernel"
445		end
446
447		return tryLoad(bootfile)
448	end
449
450	-- kernel not set, try load from default module_path
451	if kernel == nil then
452		local res = loadBootfile()
453
454		if res ~= nil then
455			-- Default kernel is loaded
456			config.kernel_loaded = nil
457			return true
458		else
459			print(MSG_DEFAULTKERNFAIL)
460			return false
461		end
462	else
463		-- Use our cached module_path, so we don't end up with multiple
464		-- automatically added kernel paths to our final module_path
465		local module_path = config.module_path
466		local res
467
468		if other_kernel ~= nil then
469			kernel = other_kernel
470		end
471		-- first try load kernel with module_path = /boot/${kernel}
472		-- then try load with module_path=${kernel}
473		local paths = {"/boot/" .. kernel, kernel}
474
475		for _, v in pairs(paths) do
476			loader.setenv("module_path", v)
477			res = loadBootfile()
478
479			-- succeeded, add path to module_path
480			if res ~= nil then
481				config.kernel_loaded = kernel
482				if module_path ~= nil then
483					loader.setenv("module_path", v .. ";" ..
484					    module_path)
485				end
486				return true
487			end
488		end
489
490		-- failed to load with ${kernel} as a directory
491		-- try as a file
492		res = tryLoad(kernel)
493		if res ~= nil then
494			config.kernel_loaded = kernel
495			return true
496		else
497			print(MSG_KERNFAIL:format(kernel))
498			return false
499		end
500	end
501end
502
503function config.selectKernel(kernel)
504	config.kernel_selected = kernel
505end
506
507function config.load(file, reloading)
508	if not file then
509		file = "/boot/defaults/loader.conf"
510	end
511
512	if not config.processFile(file) then
513		print(MSG_FAILPARSECFG:format(file))
514	end
515
516	local loaded_files = {file = true}
517	readConfFiles(loaded_files)
518
519	checkNextboot()
520
521	-- Cache the provided module_path at load time for later use
522	config.module_path = loader.getenv("module_path")
523	local verbose = loader.getenv("verbose_loading") or "no"
524	config.verbose = verbose:lower() == "yes"
525	if not reloading then
526		hook.runAll("config.loaded")
527	end
528end
529
530-- Reload configuration
531function config.reload(file)
532	modules = {}
533	restoreEnv()
534	config.load(file, true)
535	hook.runAll("config.reloaded")
536end
537
538function config.loadelf()
539	local kernel = config.kernel_selected or config.kernel_loaded
540	local loaded
541
542	print(MSG_KERNLOADING)
543	loaded = config.loadKernel(kernel)
544
545	if not loaded then
546		return
547	end
548
549	print(MSG_MODLOADING)
550	if not loadModule(modules, not config.verbose) then
551		print(MSG_MODLOADFAIL)
552	end
553end
554
555hook.registerType("config.loaded")
556hook.registerType("config.reloaded")
557return config
558