mirror of
https://github.com/nchevsky/systemrescue-zfs.git
synced 2025-12-06 07:12:01 +01:00
since recently /run/archiso/copytoram is bind-mounted to /run/archiso/bootmnt. This means sysrescue-configuration.lua should just load yaml files from /run/archiso/bootmnt and not both, otherwise they would be loaded twice.
352 lines
14 KiB
Lua
Executable file
352 lines
14 KiB
Lua
Executable file
#!/usr/bin/env lua
|
|
--
|
|
-- Author: Francois Dupoux
|
|
-- SPDX-License-Identifier: GPL-3.0-or-later
|
|
--
|
|
-- SystemRescue configuration processing script
|
|
--
|
|
-- This script uses the SystemRescue yaml configuration files and the options
|
|
-- passed on the boot command line to override the default configuration.
|
|
-- It processes yaml configuration files in the alphabetical order, and each option
|
|
-- found in a file override the options defined earlier. Options passed on the
|
|
-- boot command like take precedence over configuration options defined in files.
|
|
-- At the end it writes the effective configuration to a JSON file which is meant
|
|
-- to be ready by any initialisation script which needs to know the configuration.
|
|
-- Shell scripts can read values from the JSON file using a command such as:
|
|
-- jq --raw-output '.global.copytoram' /run/archiso/config/sysrescue-effective-config.json
|
|
-- This script requires the following lua packages to run on Arch Linux:
|
|
-- sudo pacman -Sy lua lua-yaml lua-dkjson lua-http
|
|
|
|
-- ==============================================================================
|
|
-- Import modules
|
|
-- ==============================================================================
|
|
local lfs = require('lfs')
|
|
local yaml = require('yaml')
|
|
local json = require("dkjson")
|
|
local request = require("http.request")
|
|
local tls_ctx = require "http.tls".new_client_context()
|
|
local tls_ctx_noverify = require "openssl.ssl.context".VERIFY_NONE
|
|
local tls_ctx_doverify = require "openssl.ssl.context".VERIFY_PEER
|
|
|
|
-- ==============================================================================
|
|
-- Utility functions
|
|
-- ==============================================================================
|
|
-- Read a file and return all its contents
|
|
function read_file_contents(path)
|
|
local file = io.open(path, "rb")
|
|
if not file then
|
|
return nil
|
|
end
|
|
local content = file:read("*a")
|
|
file:close()
|
|
return content
|
|
end
|
|
|
|
-- Return true if the item is present in the list or false otherwise
|
|
function item_in_list(item, list)
|
|
for _, curitem in ipairs(list) do
|
|
if (curitem == item) then
|
|
return true
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
|
|
-- Ensure that the given scope exists in the config table, create it if not
|
|
function ensure_scope(cfg_table, scopename)
|
|
if (cfg_table == nil) or (type(cfg_table) ~= "table") then
|
|
cfg_table = { }
|
|
end
|
|
if (cfg_table[scopename] == nil) or (type(cfg_table[scopename]) ~= "table") then
|
|
cfg_table[scopename] = { }
|
|
end
|
|
end
|
|
|
|
-- Return the number of items in a table
|
|
function get_table_size(mytable)
|
|
size = 0
|
|
for _ in pairs(mytable) do
|
|
size = size + 1
|
|
end
|
|
return size
|
|
end
|
|
|
|
-- Return a list of files with a yaml extension found in the directory 'dirname'
|
|
-- If 'filenames' is an empty list then it will return all files which have been found
|
|
-- If 'filenames' is not empty then it will only return files with a name present in the list
|
|
function list_config_files(dirname, filenames)
|
|
local results = {}
|
|
for curfile in lfs.dir(dirname) do
|
|
fullpath = dirname.."/"..curfile
|
|
filetype = lfs.attributes(fullpath, "mode")
|
|
if (filetype == "file") and curfile:match(".[Yy][Aa][Mm][Ll]$") then
|
|
if (get_table_size(filenames) == 0) or item_in_list(curfile, filenames) then
|
|
table.insert(results, fullpath)
|
|
end
|
|
end
|
|
end
|
|
table.sort(results)
|
|
return results
|
|
end
|
|
|
|
-- Attempt to find the option 'optname' on the boot command line and return its value
|
|
-- If 'multiple' is false then it will return the value of the last occurence found or nil
|
|
-- If 'multiple' is true then it will return a list of all values passed or an empty list
|
|
function search_cmdline_option(optname, multiple)
|
|
local result_single = nil
|
|
local result_multiple = {}
|
|
local cmdline = read_file_contents("/proc/cmdline")
|
|
for curopt in cmdline:gmatch("%S+") do
|
|
optmatch1 = string.match(curopt, "^"..optname.."$")
|
|
_, _, optmatch2 = string.find(curopt, "^"..optname.."=([^%s]+)$")
|
|
if (optmatch1 ~= nil) or (optmatch2 == 'y') or (optmatch2 == 'yes') or (optmatch2 == 'true') then
|
|
result_single = true
|
|
table.insert(result_multiple, true)
|
|
elseif (optmatch2 == 'n') or (optmatch2 == 'no') or (optmatch2 == 'false') then
|
|
result_single = false
|
|
table.insert(result_multiple, false)
|
|
elseif (optmatch2 ~= nil) then
|
|
result_single = optmatch2
|
|
table.insert(result_multiple, optmatch2)
|
|
end
|
|
end
|
|
if multiple == true then
|
|
return result_multiple
|
|
else
|
|
return result_single
|
|
end
|
|
end
|
|
|
|
-- Process a block of yaml configuration and override the current configuration with new values
|
|
function process_yaml_config(config_content)
|
|
if (config_content == nil) then
|
|
io.stderr:write(string.format("Error downloading or empty file received\n"))
|
|
return false
|
|
end
|
|
if pcall(function() curconfig = yaml.load(config_content) end) then
|
|
if (curconfig == nil) or (type(curconfig) ~= "table") then
|
|
io.stderr:write(string.format("This is not valid yaml (=no table), it will be ignored\n"))
|
|
return false
|
|
end
|
|
merge_config_table(config, curconfig, "config")
|
|
return true
|
|
else
|
|
io.stderr:write(string.format("Failed parsing yaml, it will be ignored\n"))
|
|
return false
|
|
end
|
|
end
|
|
|
|
-- Recursive merge of a config table
|
|
-- config_table: references the current level within the global config
|
|
-- new_table: the current level within the new yaml we want to merge right now
|
|
-- leveltext: textual representation of the current level used for messages, split by "|"
|
|
function merge_config_table(config_table, new_table, leveltext)
|
|
for key, value in pairs(new_table) do
|
|
-- loop through the current level of the new config
|
|
if (config_table[key] == nil) then
|
|
-- a key just existing in the new config, not in current config -> copy it
|
|
print("- Merging "..leveltext.."|"..key.." into the config")
|
|
config_table[key] = value
|
|
else
|
|
-- key of the new config also exisiting in the current config: check value type
|
|
if (type(value) == "nil" or (type(value) == "string" and value == "")) then
|
|
-- remove an existing table entry with an empty value
|
|
print("- Removing "..leveltext.."|"..key)
|
|
config_table[key] = nil
|
|
elseif (type(value) == "table" and type(config_table[key]) == "table") then
|
|
-- old and new values are tables: recurse
|
|
merge_config_table(config_table[key], value, leveltext.."|"..key)
|
|
else
|
|
-- overwrite the old value
|
|
print("- Overriding "..leveltext.."|"..key.." with the value from the yaml file")
|
|
config_table[key] = value
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Download a file over http/https and return the contents of the file or nil if it fails
|
|
function download_file(fileurl)
|
|
local req_timeout = 10
|
|
local req = request.new_from_uri(fileurl)
|
|
|
|
--- we (usually) run during initramfs where the CA database is not available, so don't verify certificates
|
|
tls_ctx:setVerify(tls_ctx_noverify)
|
|
req.ctx = tls_ctx
|
|
|
|
local headers, stream = req:go(req_timeout)
|
|
|
|
if headers == nil then
|
|
--- the second return variable (=stream) contains the error message in case of an error
|
|
io.stderr:write(string.format("Failed to download %s: %s\n", fileurl, stream))
|
|
return nil
|
|
end
|
|
|
|
status = headers:get(":status")
|
|
if status ~= '200' then
|
|
io.stderr:write(string.format("Failed to download %s: Received HTTP code %s\n", fileurl, status))
|
|
return nil
|
|
end
|
|
|
|
local body, err = stream:get_body_as_string()
|
|
if not body and err then
|
|
io.stderr:write(string.format("Failed to download %s: Error %s\n", fileurl, tostring(err)))
|
|
return nil
|
|
end
|
|
|
|
return body
|
|
end
|
|
|
|
-- ==============================================================================
|
|
-- Initialisation
|
|
-- ==============================================================================
|
|
errcnt = 0
|
|
|
|
-- ==============================================================================
|
|
-- We start with an empty global config
|
|
-- the default config is usually in the first yaml file parsed (100-defaults.yaml)
|
|
-- ==============================================================================
|
|
config = { }
|
|
|
|
-- ==============================================================================
|
|
-- Merge one yaml file after the other in lexicographic order
|
|
-- ==============================================================================
|
|
print ("====> Merging configuration with values from yaml files ...")
|
|
-- bootmnt is bind-mounted in case of copytoram, so it doesn't need to be searched explicitly
|
|
confdirs = {"/run/archiso/bootmnt/sysrescue.d"}
|
|
|
|
-- Process local yaml configuration files
|
|
for _, curdir in ipairs(confdirs) do
|
|
if lfs.attributes(curdir, "mode") == "directory" then
|
|
print("Searching for yaml configuration files in "..curdir.." ...")
|
|
for _, curfile in ipairs(list_config_files(curdir, {})) do
|
|
print(string.format("Processing local yaml configuration file: %s ...", curfile))
|
|
if process_yaml_config(read_file_contents(curfile)) == false then
|
|
errcnt = errcnt + 1
|
|
end
|
|
end
|
|
else
|
|
print("Directory "..curdir.." was not found so it has been ignored")
|
|
end
|
|
end
|
|
|
|
-- Process explicitly configured configuration files
|
|
-- these are parsed afterwards and in the order given, so they have precedence
|
|
conffiles = search_cmdline_option("sysrescuecfg", true)
|
|
print("Searching for remote yaml configuration files ...")
|
|
for _, curfile in ipairs(conffiles) do
|
|
if string.match(curfile, "^https?://") then
|
|
print(string.format("Processing remote yaml configuration file: %s ...", curfile))
|
|
local contents = download_file(curfile)
|
|
if process_yaml_config(contents) == false then
|
|
errcnt = errcnt + 1
|
|
end
|
|
elseif string.match(curfile, "^/") then
|
|
-- we have a local file with absolute path
|
|
print(string.format("Processing local yaml configuration file: %s ...",curfile))
|
|
if process_yaml_config(read_file_contents(curfile)) == false then
|
|
errcnt = errcnt + 1
|
|
end
|
|
else
|
|
-- we have a local file with relative path, prefix the one existing config dir
|
|
-- this will apply the config again, but later than before, giving it higher priority
|
|
for _, curdir in ipairs(confdirs) do
|
|
if lfs.attributes(curdir, "mode") == "directory" then
|
|
print(string.format("Processing local yaml configuration file: %s ...",curdir.."/"..curfile))
|
|
if process_yaml_config(read_file_contents(curdir.."/"..curfile)) == false then
|
|
errcnt = errcnt + 1
|
|
end
|
|
-- just try the explicitly configured filename with one dir prefix
|
|
break
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- ==============================================================================
|
|
-- Override the configuration with values passed on the boot command line
|
|
--
|
|
-- NOTE: boot command line options are only for legacy compatibility and
|
|
-- very common options. Consider carfully before adding new boot
|
|
-- command line options. New features should by default just be
|
|
-- configured through the yaml config.
|
|
-- ==============================================================================
|
|
|
|
cmdline_options = {
|
|
['copytoram'] = "global",
|
|
['cow_label'] = "global",
|
|
['cow_directory'] = "global",
|
|
['checksum'] = "global",
|
|
['loadsrm'] = "global",
|
|
['dostartx'] = "global",
|
|
['dovnc'] = "global",
|
|
['noautologin'] = "global",
|
|
['nofirewall'] = "global",
|
|
['rootshell'] = "global",
|
|
['rootpass'] = "global",
|
|
['rootcryptpass'] = "global",
|
|
['setkmap'] = "global",
|
|
['vncpass'] = "global",
|
|
['ar_disable'] = "autorun",
|
|
['ar_nowait'] = "autorun",
|
|
['ar_nodel'] = "autorun",
|
|
['ar_ignorefail'] = "autorun",
|
|
['ar_attempts'] = "autorun",
|
|
['ar_source'] = "autorun",
|
|
['ar_suffixes'] = "autorun"
|
|
}
|
|
|
|
print ("====> Overriding the configuration with options passed on the boot command line ...")
|
|
for option, scope in pairs(cmdline_options) do
|
|
optresult = search_cmdline_option(option, false)
|
|
if optresult == true then
|
|
print("- Option '"..option.."' has been enabled on the boot command line")
|
|
ensure_scope(config, scope)
|
|
config[scope][option] = optresult
|
|
elseif optresult == false then
|
|
print("- Option '"..option.."' has been disabled on the boot command line")
|
|
ensure_scope(config, scope)
|
|
config[scope][option] = optresult
|
|
elseif optresult ~= nil then
|
|
print("- Option '"..option.."' has been defined as '"..optresult.."' on the boot command line")
|
|
ensure_scope(config, scope)
|
|
config[scope][option] = optresult
|
|
end
|
|
end
|
|
|
|
-- ==============================================================================
|
|
-- Print the effective configuration
|
|
-- ==============================================================================
|
|
print ("====> Printing the effective configuration")
|
|
local jsoncfgtxt = json.encode (config, { indent = true })
|
|
print (jsoncfgtxt)
|
|
|
|
-- ==============================================================================
|
|
-- Write the effective configuration to a JSON file
|
|
-- ==============================================================================
|
|
print ("====> Writing the effective configuration to a JSON file ...")
|
|
output_location = "/run/archiso/config"
|
|
output_filename = "sysrescue-effective-config.json"
|
|
output_fullpath = output_location.."/"..output_filename
|
|
jsoncfgfile = io.open(output_fullpath, "w")
|
|
if jsoncfgfile == nil then
|
|
io.stderr:write(string.format("ERROR: Failed to create effective configuration file in %s\n", output_fullpath))
|
|
os.exit(1)
|
|
end
|
|
jsoncfgfile:write(jsoncfgtxt)
|
|
jsoncfgfile:close()
|
|
os.execute("chmod 700 "..output_location)
|
|
os.execute("chmod 600 "..output_fullpath)
|
|
print ("Effective configuration has been written to "..output_fullpath)
|
|
|
|
-- ==============================================================================
|
|
-- Error handling
|
|
-- ==============================================================================
|
|
if errcnt == 0 then
|
|
print ("SUCCESS: Have successfully completed the processing of the configuration")
|
|
os.exit(0)
|
|
else
|
|
io.stderr:write(string.format("FAILURE: Have completed the processing of the configuration with %d errors\n", errcnt))
|
|
os.exit(1)
|
|
end
|