xref: /freebsd/stand/lua/config.lua (revision 3982006ed5587cfd83cc1955186e0aa4b92b3fcd)
1--
2-- Copyright (c) 2015 Pedro Souza <pedrosouza@freebsd.org>
3-- Copyright (C) 2018 Kyle Evans <kevans@FreeBSD.org>
4-- All rights reserved.
5--
6-- Redistribution and use in source and binary forms, with or without
7-- modification, are permitted provided that the following conditions
8-- are met:
9-- 1. Redistributions of source code must retain the above copyright
10--    notice, this list of conditions and the following disclaimer.
11-- 2. Redistributions in binary form must reproduce the above copyright
12--    notice, this list of conditions and the following disclaimer in the
13--    documentation and/or other materials provided with the distribution.
14--
15-- THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
16-- ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
17-- IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
18-- ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
19-- FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
20-- DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
21-- OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
22-- HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
23-- LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
24-- OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
25-- SUCH DAMAGE.
26--
27-- $FreeBSD$
28--
29
30local config = {}
31
32local modules = {}
33
34local pattern_table
35local carousel_choices = {}
36
37pattern_table = {
38	[1] = {
39		str = "^%s*(#.*)",
40		process = function(k, v)  end
41	},
42	--  module_load="value"
43	[2] = {
44		str = "^%s*([%w_]+)_load%s*=%s*\"([%w%s%p]-)\"%s*(.*)",
45		process = function(k, v)
46			if modules[k] == nil then
47				modules[k] = {}
48			end
49			modules[k].load = v:upper()
50		end
51	},
52	--  module_name="value"
53	[3] = {
54		str = "^%s*([%w_]+)_name%s*=%s*\"([%w%s%p]-)\"%s*(.*)",
55		process = function(k, v)
56			config.setKey(k, "name", v)
57		end
58	},
59	--  module_type="value"
60	[4] = {
61		str = "^%s*([%w_]+)_type%s*=%s*\"([%w%s%p]-)\"%s*(.*)",
62		process = function(k, v)
63			config.setKey(k, "type", v)
64		end
65	},
66	--  module_flags="value"
67	[5] = {
68		str = "^%s*([%w_]+)_flags%s*=%s*\"([%w%s%p]-)\"%s*(.*)",
69		process = function(k, v)
70			config.setKey(k, "flags", v)
71		end
72	},
73	--  module_before="value"
74	[6] = {
75		str = "^%s*([%w_]+)_before%s*=%s*\"([%w%s%p]-)\"%s*(.*)",
76		process = function(k, v)
77			config.setKey(k, "before", v)
78		end
79	},
80	--  module_after="value"
81	[7] = {
82		str = "^%s*([%w_]+)_after%s*=%s*\"([%w%s%p]-)\"%s*(.*)",
83		process = function(k, v)
84			config.setKey(k, "after", v)
85		end
86	},
87	--  module_error="value"
88	[8] = {
89		str = "^%s*([%w_]+)_error%s*=%s*\"([%w%s%p]-)\"%s*(.*)",
90		process = function(k, v)
91			config.setKey(k, "error", v)
92		end
93	},
94	--  exec="command"
95	[9] = {
96		str = "^%s*exec%s*=%s*\"([%w%s%p]-)\"%s*(.*)",
97		process = function(k, v)
98			if loader.perform(k) ~= 0 then
99				print("Failed to exec '" .. k .. "'")
100			end
101		end
102	},
103	--  env_var="value"
104	[10] = {
105		str = "^%s*([%w%p]+)%s*=%s*\"([%w%s%p]-)\"%s*(.*)",
106		process = function(k, v)
107			if config.setenv(k, v) ~= 0 then
108				print("Failed to set '" .. k ..
109				    "' with value: " .. v .. "")
110			end
111		end
112	},
113	--  env_var=num
114	[11] = {
115		str = "^%s*([%w%p]+)%s*=%s*(%d+)%s*(.*)",
116		process = function(k, v)
117			if config.setenv(k, v) ~= 0 then
118				print("Failed to set '" .. k ..
119				    "' with value: " .. v .. "")
120			end
121		end
122	}
123}
124
125-- Module exports
126-- Which variables we changed
127config.env_changed = {}
128-- Values to restore env to (nil to unset)
129config.env_restore = {}
130
131-- The first item in every carousel is always the default item.
132function config.getCarouselIndex(id)
133	local val = carousel_choices[id]
134	if val == nil then
135		return 1
136	end
137	return val
138end
139
140function config.setCarouselIndex(id, idx)
141	carousel_choices[id] = idx
142end
143
144function config.restoreEnv()
145	-- Examine changed environment variables
146	for k, v in pairs(config.env_changed) do
147		local restore_value = config.env_restore[k]
148		if restore_value == nil then
149			-- This one doesn't need restored for some reason
150			goto continue
151		end
152		local current_value = loader.getenv(k)
153		if current_value ~= v then
154			-- This was overwritten by some action taken on the menu
155			-- most likely; we'll leave it be.
156			goto continue
157		end
158		restore_value = restore_value.value
159		if restore_value ~= nil then
160			loader.setenv(k, restore_value)
161		else
162			loader.unsetenv(k)
163		end
164		::continue::
165	end
166
167	config.env_changed = {}
168	config.env_restore = {}
169end
170
171function config.setenv(k, v)
172	-- Track the original value for this if we haven't already
173	if config.env_restore[k] == nil then
174		config.env_restore[k] = {value = loader.getenv(k)}
175	end
176
177	config.env_changed[k] = v
178
179	return loader.setenv(k, v)
180end
181
182function config.setKey(k, n, v)
183	if modules[k] == nil then
184		modules[k] = {}
185	end
186	modules[k][n] = v
187end
188
189function config.lsModules()
190	print("== Listing modules")
191	for k, v in pairs(modules) do
192		print(k, v.load)
193	end
194	print("== List of modules ended")
195end
196
197
198function config.isValidComment(c)
199	if c ~= nil then
200		local s = c:match("^%s*#.*")
201		if s == nil then
202			s = c:match("^%s*$")
203		end
204		if s == nil then
205			return false
206		end
207	end
208	return true
209end
210
211function config.loadmod(mod, silent)
212	local status = true
213	for k, v in pairs(mod) do
214		if v.load == "YES" then
215			local str = "load "
216			if v.flags ~= nil then
217				str = str .. v.flags .. " "
218			end
219			if v.type ~= nil then
220				str = str .. "-t " .. v.type .. " "
221			end
222			if v.name ~= nil then
223				str = str .. v.name
224			else
225				str = str .. k
226			end
227
228			if v.before ~= nil then
229				if loader.perform(v.before) ~= 0 then
230					if not silent then
231						print("Failed to execute '" ..
232						    v.before ..
233						    "' before loading '" .. k ..
234						    "'")
235					end
236					status = false
237				end
238			end
239
240			if loader.perform(str) ~= 0 then
241				if not silent then
242					print("Failed to execute '" .. str ..
243					    "'")
244				end
245				if v.error ~= nil then
246					loader.perform(v.error)
247				end
248				status = false
249			end
250
251			if v.after ~= nil then
252				if loader.perform(v.after) ~= 0 then
253					if not silent then
254						print("Failed to execute '" ..
255						    v.after ..
256						    "' after loading '" .. k ..
257						    "'")
258					end
259					status = false
260				end
261			end
262
263		else
264			-- if not silent then
265				-- print("Skipping module '". . k .. "'")
266			-- end
267		end
268	end
269
270	return status
271end
272
273-- silent runs will not return false if we fail to open the file
274function config.parse(name, silent)
275	if silent == nil then
276		silent = false
277	end
278	local f = io.open(name)
279	if f == nil then
280		if not silent then
281			print("Failed to open config: '" .. name .. "'")
282		end
283		return silent
284	end
285
286	local text
287	local r
288
289	text, r = io.read(f)
290
291	if text == nil then
292		if not silent then
293			print("Failed to read config: '" .. name .. "'")
294		end
295		return silent
296	end
297
298	local n = 1
299	local status = true
300
301	for line in text:gmatch("([^\n]+)") do
302		if line:match("^%s*$") == nil then
303			local found = false
304
305			for i, val in ipairs(pattern_table) do
306				local k, v, c = line:match(val.str)
307				if k ~= nil then
308					found = true
309
310					if config.isValidComment(c) then
311						val.process(k, v)
312					else
313						print("Malformed line (" .. n ..
314						    "):\n\t'" .. line .. "'")
315						status = false
316					end
317
318					break
319				end
320			end
321
322			if not found then
323				print("Malformed line (" .. n .. "):\n\t'" ..
324				    line .. "'")
325				status = false
326			end
327		end
328		n = n + 1
329	end
330
331	return status
332end
333
334-- other_kernel is optionally the name of a kernel to load, if not the default
335-- or autoloaded default from the module_path
336function config.loadkernel(other_kernel)
337	local flags = loader.getenv("kernel_options") or ""
338	local kernel = other_kernel or loader.getenv("kernel")
339
340	local try_load = function (names)
341		for name in names:gmatch("([^;]+)%s*;?") do
342			r = loader.perform("load " .. flags .. " " .. name)
343			if r == 0 then
344				return name
345			end
346		end
347		return nil
348	end
349
350	local load_bootfile = function()
351		local bootfile = loader.getenv("bootfile")
352
353		-- append default kernel name
354		if bootfile == nil then
355			bootfile = "kernel"
356		else
357			bootfile = bootfile .. ";kernel"
358		end
359
360		return try_load(bootfile)
361	end
362
363	-- kernel not set, try load from default module_path
364	if kernel == nil then
365		local res = load_bootfile()
366
367		if res ~= nil then
368			-- Default kernel is loaded
369			config.kernel_loaded = nil
370			return true
371		else
372			print("No kernel set, failed to load from module_path")
373			return false
374		end
375	else
376		-- Use our cached module_path, so we don't end up with multiple
377		-- automatically added kernel paths to our final module_path
378		local module_path = config.module_path
379		local res = nil
380
381		if other_kernel ~= nil then
382			kernel = other_kernel
383		end
384		-- first try load kernel with module_path = /boot/${kernel}
385		-- then try load with module_path=${kernel}
386		local paths = {"/boot/" .. kernel, kernel}
387
388		for k,v in pairs(paths) do
389			loader.setenv("module_path", v)
390			res = load_bootfile()
391
392			-- succeeded, add path to module_path
393			if res ~= nil then
394				config.kernel_loaded = kernel
395				if module_path ~= nil then
396					loader.setenv("module_path", v .. ";" ..
397					    module_path)
398				end
399				return true
400			end
401		end
402
403		-- failed to load with ${kernel} as a directory
404		-- try as a file
405		res = try_load(kernel)
406		if res ~= nil then
407			config.kernel_loaded = kernel
408			return true
409		else
410			print("Failed to load kernel '" .. kernel .. "'")
411			return false
412		end
413	end
414end
415
416function config.selectkernel(kernel)
417	config.kernel_selected = kernel
418end
419
420function config.load(file)
421	if not file then
422		file = "/boot/defaults/loader.conf"
423	end
424
425	if not config.parse(file) then
426		print("Failed to parse configuration: '" .. file .. "'")
427	end
428
429	local f = loader.getenv("loader_conf_files")
430	if f ~= nil then
431		for name in f:gmatch("([%w%p]+)%s*") do
432			-- These may or may not exist, and that's ok. Do a
433			-- silent parse so that we complain on parse errors but
434			-- not for them simply not existing.
435			if not config.parse(name, true) then
436				print("Failed to parse configuration: '" ..
437				    name .. "'")
438			end
439		end
440	end
441
442	-- Cache the provided module_path at load time for later use
443	config.module_path = loader.getenv("module_path")
444end
445
446-- Reload configuration
447function config.reload(file)
448	modules = {}
449	config.restoreEnv()
450	config.load(file)
451end
452
453function config.loadelf()
454	local kernel = config.kernel_selected or config.kernel_loaded
455	local loaded = false
456
457	print("Loading kernel...")
458	loaded = config.loadkernel(kernel)
459
460	if not loaded then
461		print("Failed to load any kernel")
462		return
463	end
464
465	print("Loading configured modules...")
466	if not config.loadmod(modules) then
467		print("Could not load one or more modules!")
468	end
469end
470
471return config
472