diff --git a/libexec/nuageinit/nuage.lua b/libexec/nuageinit/nuage.lua index deb441ee25b..cdc0fc6cf2a 100644 --- a/libexec/nuageinit/nuage.lua +++ b/libexec/nuageinit/nuage.lua @@ -7,6 +7,39 @@ local unistd = require("posix.unistd") local sys_stat = require("posix.sys.stat") local lfs = require("lfs") +local function decode_base64(input) + local b = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/' + input = string.gsub(input, '[^'..b..'=]', '') + + local result = {} + local bits = '' + + -- convert all characters in bits + for i = 1, #input do + local x = input:sub(i, i) + if x == '=' then + break + end + local f = b:find(x) - 1 + for j = 6, 1, -1 do + bits = bits .. (f % 2^j - f % 2^(j-1) > 0 and '1' or '0') + end + end + + for i = 1, #bits, 8 do + local byte = bits:sub(i, i + 7) + if #byte == 8 then + local c = 0 + for j = 1, 8 do + c = c + (byte:sub(j, j) == '1' and 2^(8 - j) or 0) + end + table.insert(result, string.char(c)) + end + end + + return table.concat(result) +end + local function warnmsg(str, prepend) if not str then return @@ -441,6 +474,58 @@ local function upgrade_packages() return run_pkg_cmd("upgrade") end +local function addfile(file, defer) + if type(file) ~= "table" then + return false, "Invalid object" + end + if defer and not file.defer then + return true + end + if not defer and file.defer then + return true + end + if not file.path then + return false, "No path provided for the file to write" + end + local content = nil + if file.content then + if file.encoding then + if file.encoding == "b64" or file.encoding == "base64" then + content = decode_base64(file.content) + else + return false, "Unsupported encoding: " .. file.encoding + end + else + content = file.content + end + end + local mode = "w" + if file.append then + mode = "a" + end + + local root = os.getenv("NUAGE_FAKE_ROOTDIR") + if not root then + root = "" + end + local filepath = root .. file.path + local f = assert(io.open(filepath, mode)) + if content then + f:write(content) + end + f:close() + if file.permissions then + -- convert from octal to decimal + local perm = tonumber(file.permissions, 8) + sys_stat.chmod(file.path, perm) + end + if file.owner then + local owner, group = string.match(file.owner, "([^:]+):([^:]+)") + unistd.chown(file.path, owner, group) + end + return true +end + local n = { warn = warnmsg, err = errmsg, @@ -456,7 +541,8 @@ local n = { install_package = install_package, update_packages = update_packages, upgrade_packages = upgrade_packages, - addsudo = addsudo + addsudo = addsudo, + addfile = addfile } return n diff --git a/libexec/nuageinit/nuageinit b/libexec/nuageinit/nuageinit index 5af1b84c184..84133d4373c 100755 --- a/libexec/nuageinit/nuageinit +++ b/libexec/nuageinit/nuageinit @@ -188,6 +188,25 @@ local function install_packages(packages) end end +local function write_files(files, defer) + if not files then + return + end + for n, file in pairs(files) do + local r, errstr = nuage.addfile(file, defer) + if not r then + nuage.warn("Skipping write_files entry number " .. n .. ": " .. errstr) + end + end +end + +local function write_files_not_defered(obj) + write_files(obj.write_files, false) +end + +local function write_files_defered(obj) + write_files(obj.write_files, true) +end -- Set network configuration from user_data local function network_config(obj) if obj.network == nil then return end @@ -456,13 +475,15 @@ if line == "#cloud-config" then ssh_authorized_keys, network_config, ssh_pwauth, - runcmd + runcmd, + write_files_not_defered, } local post_network_calls = { packages, users, - chpasswd + chpasswd, + write_files_defered, } f = io.open(ni_path .. "/" .. ud) diff --git a/libexec/nuageinit/nuageinit.7 b/libexec/nuageinit/nuageinit.7 index 1d2f83fe62e..3bb440ebac9 100644 --- a/libexec/nuageinit/nuageinit.7 +++ b/libexec/nuageinit/nuageinit.7 @@ -2,7 +2,7 @@ .\" .\" Copyright (c) 2025 Baptiste Daroussin .\" -.Dd June 16, 2025 +.Dd June 26, 2025 .Dt NUAGEINIT 7 .Os .Sh NAME @@ -239,6 +239,42 @@ where x is a number, then the password is considered encrypted, otherwise the password is considered plaintext. .El .El +.It Ic write_files +An array of objects representing files to be created at first boot. +The files are being created before the installation of any packages +and the creation of the users. +The only mandatory field is: +.Ic path . +It accepts the following keys for each objects: +.Bl -tag -width "permissions" +.It Ic content +The content to be written to the file. +If this key is not existing then an empty file will be created. +.It Ic encoding +Specifiy the encoding used for content. +If not specified, then plain text is considered. +Only +.Ar b64 +and +.Ar base64 +are supported for now. +.It Ic path +The path of the file to be created. +.Pq Note intermerdiary directories will not be created . +.It Ic permissions +A string representing the permission of the file in octal. +.It Ic owner +A string representing the owner, two forms are possible: +.Ar user +or +.Ar user:group . +.It Ic append +A boolean to specify the content should be appended to the file if the file +exists. +.It Ic defer +A boolean to specify that the files should be created after the packages are +installed and the users are created. +.El .El .Sh EXAMPLES Here is an example of a YAML configuration for diff --git a/libexec/nuageinit/tests/Makefile b/libexec/nuageinit/tests/Makefile index bb2f0d7c747..c69bc28a4c8 100644 --- a/libexec/nuageinit/tests/Makefile +++ b/libexec/nuageinit/tests/Makefile @@ -16,5 +16,6 @@ ${PACKAGE}FILES+= dirname.lua ${PACKAGE}FILES+= err.lua ${PACKAGE}FILES+= sethostname.lua ${PACKAGE}FILES+= warn.lua +${PACKAGE}FILES+= addfile.lua .include diff --git a/libexec/nuageinit/tests/addfile.lua b/libexec/nuageinit/tests/addfile.lua new file mode 100644 index 00000000000..98d020e557c --- /dev/null +++ b/libexec/nuageinit/tests/addfile.lua @@ -0,0 +1,71 @@ +#!/bin/libexec/flua + +local n = require("nuage") +local lfs = require("lfs") + +local f = { + content = "plop" +} + +local r, err = n.addfile(f, false) +if r or err ~= "No path provided for the file to write" then + n.err("addfile should not accept a file to write without a path") +end + +local function addfile_and_getres(file) + local r, err = n.addfile(file, false) + if not r then + n.err(err) + end + local root = os.getenv("NUAGE_FAKE_ROOTDIR") + if not root then + root = "" + end + local filepath = root .. file.path + local resf = assert(io.open(filepath, "r")) + local str = resf:read("*all") + resf:close() + return str +end + +-- simple file +f.path="/tmp/testnuage" +local str = addfile_and_getres(f) +if str ~= f.content then + n.err("Invalid file content") +end + +-- the file is overwriten +f.content = "test" + +str = addfile_and_getres(f) +if str ~= f.content then + n.err("Invalid file content, not overwritten") +end + +-- try to append now +f.content = "more" +f.append = true + +str = addfile_and_getres(f) +if str ~= "test" .. f.content then + n.err("Invalid file content, not appended") +end + +-- base64 +f.content = "YmxhCg==" +f.encoding = "base64" +f.append = false + +str = addfile_and_getres(f) +if str ~= "bla\n" then + n.err("Invalid file content, base64 decode") +end + +-- b64 +f.encoding = "b64" +str = addfile_and_getres(f) +if str ~= "bla\n" then + n.err("Invalid file content, b64 decode") + print("==>" .. str .. "<==") +end diff --git a/libexec/nuageinit/tests/nuage.sh b/libexec/nuageinit/tests/nuage.sh index f2753d6d91e..56651c8c5bb 100644 --- a/libexec/nuageinit/tests/nuage.sh +++ b/libexec/nuageinit/tests/nuage.sh @@ -1,5 +1,5 @@ #- -# Copyright (c) 2022 Baptiste Daroussin +# Copyright (c) 2022-2025 Baptiste Daroussin # # SPDX-License-Identifier: BSD-2-Clause # @@ -11,6 +11,7 @@ atf_test_case addsshkey atf_test_case adduser atf_test_case adduser_passwd atf_test_case addgroup +atf_test_case addfile sethostname_body() { @@ -73,6 +74,12 @@ addgroup_body() atf_check -o inline:"impossible_groupname:*:1001:\n" grep impossible_groupname etc/group } +addfile_body() +{ + mkdir tmp + atf_check /usr/libexec/flua $(atf_get_srcdir)/addfile.lua +} + atf_init_test_cases() { atf_add_test_case sethostname @@ -80,4 +87,5 @@ atf_init_test_cases() atf_add_test_case adduser atf_add_test_case adduser_passwd atf_add_test_case addgroup + atf_add_test_case addfile } diff --git a/libexec/nuageinit/tests/nuageinit.sh b/libexec/nuageinit/tests/nuageinit.sh index 44830f67e4c..639c87181f9 100644 --- a/libexec/nuageinit/tests/nuageinit.sh +++ b/libexec/nuageinit/tests/nuageinit.sh @@ -1,5 +1,5 @@ #- -# Copyright (c) 2022 Baptiste Daroussin +# Copyright (c) 2022-2025 Baptiste Daroussin # # SPDX-License-Identifier: BSD-2-Clause # @@ -29,6 +29,7 @@ atf_test_case config2_userdata_update_packages atf_test_case config2_userdata_upgrade_packages atf_test_case config2_userdata_shebang atf_test_case config2_userdata_fqdn_and_hostname +atf_test_case config2_userdata_write_files setup_test_adduser() { @@ -847,6 +848,39 @@ EOF fi } +config2_userdata_write_files_body() +{ + mkdir -p media/nuageinit + setup_test_adduser + printf "{}" > media/nuageinit/meta_data.json + cat > media/nuageinit/user_data <