The branch main has been updated by bapt: URL: https://cgit.FreeBSD.org/src/commit/?id=19a7ea3cc4de5af80e2913fda70bd65ad72835c0
commit 19a7ea3cc4de5af80e2913fda70bd65ad72835c0 Author: Baptiste Daroussin <b...@freebsd.org> AuthorDate: 2025-06-26 11:32:07 +0000 Commit: Baptiste Daroussin <b...@freebsd.org> CommitDate: 2025-06-26 11:47:37 +0000 nuageinit: implement write_files write_files is a list of files that should be created at the first boot each file content can be either plain text or encoded in base64 (note that cloudinit specify that gzip is supported, but we do not support it yet.) All other specifier from cloudinit should work: by default all files will juste overwrite exesiting files except if "append" is set to true, permissions, ownership can be specified. The files are create before packages are being installed and user created. if "defer" is set to true then the file is being created after packages installation and package manupulation. This feature is requested for KDE's CI. --- libexec/nuageinit/nuage.lua | 88 +++++++++++++++++++++++++++++++++++- libexec/nuageinit/nuageinit | 25 +++++++++- libexec/nuageinit/nuageinit.7 | 38 +++++++++++++++- libexec/nuageinit/tests/Makefile | 1 + libexec/nuageinit/tests/addfile.lua | 71 +++++++++++++++++++++++++++++ libexec/nuageinit/tests/nuage.sh | 10 +++- libexec/nuageinit/tests/nuageinit.sh | 37 ++++++++++++++- 7 files changed, 264 insertions(+), 6 deletions(-) diff --git a/libexec/nuageinit/nuage.lua b/libexec/nuageinit/nuage.lua index deb441ee25ba..cdc0fc6cf2a7 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 5af1b84c1848..84133d4373c5 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 1d2f83fe62e0..3bb440ebac95 100644 --- a/libexec/nuageinit/nuageinit.7 +++ b/libexec/nuageinit/nuageinit.7 @@ -2,7 +2,7 @@ .\" .\" Copyright (c) 2025 Baptiste Daroussin <b...@freebsd.org> .\" -.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 bb2f0d7c747e..c69bc28a4c86 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 <bsd.test.mk> diff --git a/libexec/nuageinit/tests/addfile.lua b/libexec/nuageinit/tests/addfile.lua new file mode 100644 index 000000000000..98d020e557c0 --- /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 f2753d6d91e6..56651c8c5bb7 100644 --- a/libexec/nuageinit/tests/nuage.sh +++ b/libexec/nuageinit/tests/nuage.sh @@ -1,5 +1,5 @@ #- -# Copyright (c) 2022 Baptiste Daroussin <b...@freebsd.org> +# Copyright (c) 2022-2025 Baptiste Daroussin <b...@freebsd.org> # # 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 44830f67e4c8..639c87181f95 100644 --- a/libexec/nuageinit/tests/nuageinit.sh +++ b/libexec/nuageinit/tests/nuageinit.sh @@ -1,5 +1,5 @@ #- -# Copyright (c) 2022 Baptiste Daroussin <b...@freebsd.org> +# Copyright (c) 2022-2025 Baptiste Daroussin <b...@freebsd.org> # # 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 <<EOF +#cloud-config +write_files: +- content: "plop" + path: /file1 +- path: /emptyfile +- content: !!binary | + YmxhCg== + path: /file_base64 + encoding: b64 + permissions: '0755' + owner: nobody +- content: "bob" + path: "/foo" + defer: true +EOF + atf_check -o empty /usr/libexec/nuageinit "${PWD}"/media/nuageinit config-2 + atf_check -o inline:"plop" cat file1 + atf_check -o inline:"" cat emptyfile + atf_check -o inline:"bla\n" cat file_base64 + test -f foo && atf_fail "foo creation should have been defered" + atf_check -o match:"^-rwxr-xr-x.*nobody" ls -l file_base64 + rm file1 emptyfile file_base64 + atf_check -o empty /usr/libexec/nuageinit "${PWD}"/media/nuageinit postnet + test -f file1 -o -f emptyfile -o -f file_base64 && atf_fail "defer not working properly" + atf_check -o inline:"bob" cat foo +} + config2_userdata_fqdn_and_hostname_body() { mkdir -p media/nuageinit @@ -892,4 +926,5 @@ atf_init_test_cases() atf_add_test_case config2_userdata_upgrade_packages atf_add_test_case config2_userdata_shebang atf_add_test_case config2_userdata_fqdn_and_hostname + atf_add_test_case config2_userdata_write_files }