The branch main has been updated by bapt: URL: https://cgit.FreeBSD.org/src/commit/?id=be711ade6f66506fb2cae9fd33b142ce910f0346
commit be711ade6f66506fb2cae9fd33b142ce910f0346 Author: Baptiste Daroussin <[email protected]> AuthorDate: 2026-06-05 20:45:54 +0000 Commit: Baptiste Daroussin <[email protected]> CommitDate: 2026-06-05 20:45:54 +0000 nuageinit: implement MIME multipart user-data support Add support for MIME multipart/mixed user-data, allowing a single user-data blob to contain multiple parts with different content types. --- libexec/nuageinit/nuage.lua | 45 ++++++++++++++++++++++++++++++++++++ libexec/nuageinit/nuageinit | 38 ++++++++++++++++++++++++++++++ libexec/nuageinit/nuageinit.7 | 14 +++++++++++ libexec/nuageinit/tests/nuageinit.sh | 35 ++++++++++++++++++++++++++++ 4 files changed, 132 insertions(+) diff --git a/libexec/nuageinit/nuage.lua b/libexec/nuageinit/nuage.lua index 7fde2d936f1b..6cef5d2dd904 100644 --- a/libexec/nuageinit/nuage.lua +++ b/libexec/nuageinit/nuage.lua @@ -896,6 +896,50 @@ local function remove_fstab_entry(root, mount_point) nf:close() end +local function parse_mime_multipart(data) + local boundary = data:match("boundary=\"([^\"]+)\"") + if not boundary then + boundary = data:match("boundary=([^%s;]+)") + end + if not boundary then + return nil + end + local parts = {} + local pos = data:find("\n") or 1 + local first = data:find("--" .. boundary, pos, true) + if not first then + return nil + end + pos = data:find("\n", first) + if not pos then return nil end + pos = pos + 1 + while true do + local nextb = data:find("--" .. boundary, pos, true) + if not nextb then break end + local part = data:sub(pos, nextb - 1) + part = part:gsub("^\r?\n", ""):gsub("\r?\n$", "") + local header_end = part:find("\r?\n\r?\n") + local headers_str, body + if header_end then + headers_str = part:sub(1, header_end - 1) + body = part:sub(header_end + 2):gsub("^\r?\n", ""):gsub("\r?\n$", "") + else + body = part + end + local ct = "text/plain" + if headers_str then + local m = headers_str:match("[Cc]ontent%-[Tt]ype:%s*([^%s;]+)") + if m then ct = m:lower() end + end + table.insert(parts, {content_type = ct, body = body}) + local after = data:sub(nextb + 2 + #boundary, nextb + 3 + #boundary) + if after == "--" then break end + pos = data:find("\n", nextb) or nextb + if pos then pos = pos + 1 end + end + return parts +end + local n = { shell_escape = shell_escape, warn = warnmsg, @@ -923,6 +967,7 @@ local n = { add_fstab_entry = add_fstab_entry, remove_fstab_entry = remove_fstab_entry, write_resolv_conf = write_resolv_conf, + parse_mime_multipart = parse_mime_multipart, } return n diff --git a/libexec/nuageinit/nuageinit b/libexec/nuageinit/nuageinit index f5a018a00793..bd72f02d4503 100755 --- a/libexec/nuageinit/nuageinit +++ b/libexec/nuageinit/nuageinit @@ -915,6 +915,44 @@ local function load_userdata() f:close() return end + if line:match("^Content%-Type: multipart/") then + local rest = f:read("*a") + f:close() + local full = line .. "\n" .. rest + local parts = nuage.parse_mime_multipart(full) + if parts then + local cc_body = nil + for _, p in ipairs(parts) do + if p.content_type == "text/cloud-config" then + cc_body = p.body + elseif p.content_type:match("x%-shellscript") or p.content_type:match("x%-sh") then + if citype ~= "postnet" then + nuage.mkdir_p(root .. "/var/cache/nuageinit") + local spath = root .. "/var/cache/nuageinit/multipart_script" + local sf = io.open(spath, "w") + if sf then + sf:write(p.body .. "\n") + sf:close() + nuage.chmod(spath, "0755") + end + end + end + end + if cc_body then + local obj = yaml.load(cc_body) + if obj then + if citype ~= "postnet" then + nuage.mkdir_p(root .. "/var/cache/nuageinit") + local tof = assert(io.open(root .. "/var/cache/nuageinit/user_data", "w")) + tof:write("#cloud-config\n" .. cc_body) + tof:close() + end + return "#cloud-config", obj + end + end + end + return nil, nil + end if citype ~= "postnet" then local content = f:read("*a") if not content or #string.gsub(content, "^%s*(.-)%s*$", "%1") == 0 then diff --git a/libexec/nuageinit/nuageinit.7 b/libexec/nuageinit/nuageinit.7 index fcccf1bef4f0..b4be4e4b2d58 100644 --- a/libexec/nuageinit/nuageinit.7 +++ b/libexec/nuageinit/nuageinit.7 @@ -551,6 +551,20 @@ A boolean to specify that the files should be created after the packages are installed and the users are created. .El .El +.Pp +Additionally, user-data can be provided as a MIME multipart message +with content type +.Qq multipart/mixed . +Each part is handled according to its +.Qq Content-Type +header. +Supported part types: +.Bl -tag -width "text/x-shellscript" +.It text/cloud-config +Processed as a cloud-config YAML document. +.It text/x-shellscript +Saved as an executable script for later execution. +.El .Sh EXAMPLES Here is an example of a YAML configuration for .Nm : diff --git a/libexec/nuageinit/tests/nuageinit.sh b/libexec/nuageinit/tests/nuageinit.sh index 8f746599f14f..4b751dd2ca43 100644 --- a/libexec/nuageinit/tests/nuageinit.sh +++ b/libexec/nuageinit/tests/nuageinit.sh @@ -40,6 +40,7 @@ atf_test_case config2_userdata_keyboard atf_test_case config2_userdata_ssh_authkey_fingerprints atf_test_case config2_userdata_ntp atf_test_case config2_userdata_ca_certs +atf_test_case config2_userdata_multipart atf_test_case config2_userdata_fqdn_and_hostname atf_test_case config2_userdata_write_files @@ -1274,6 +1275,39 @@ EOF true } +config2_userdata_multipart_head() +{ + atf_set "require.user" root +} +config2_userdata_multipart_body() +{ + mkdir -p media/nuageinit + setup_test_adduser + printf "{}" > media/nuageinit/meta_data.json + cat > media/nuageinit/user_data <<'EOF' +Content-Type: multipart/mixed; boundary="==BOUNDARY==" + +--==BOUNDARY== +Content-Type: text/cloud-config; charset="us-ascii" + +#cloud-config +hostname: multipart-host + +--==BOUNDARY== +Content-Type: text/x-shellscript + +#!/bin/sh +echo "multipart script executed" + +--==BOUNDARY==-- +EOF + atf_check -o empty /usr/libexec/nuageinit "${PWD}"/media/nuageinit config-2 + atf_check -o inline:"hostname=\"multipart-host\"\n" cat etc/rc.conf.d/hostname + atf_check -o inline:"#!/bin/sh\necho \"multipart script executed\"\n" cat var/cache/nuageinit/multipart_script + test -x var/cache/nuageinit/multipart_script || atf_fail "multipart_script not executable" + true +} + config2_userdata_fqdn_and_hostname_body() { mkdir -p media/nuageinit @@ -1329,6 +1363,7 @@ atf_init_test_cases() atf_add_test_case config2_userdata_ssh_authkey_fingerprints atf_add_test_case config2_userdata_ntp atf_add_test_case config2_userdata_ca_certs + atf_add_test_case config2_userdata_multipart atf_add_test_case config2_userdata_fqdn_and_hostname atf_add_test_case config2_userdata_write_files }
