Skip site navigation (1)Skip section navigation (2)
Date:      Sat, 06 Jun 2026 06:14:17 +0000
From:      Baptiste Daroussin <bapt@FreeBSD.org>
To:        src-committers@FreeBSD.org, dev-commits-src-all@FreeBSD.org, dev-commits-src-main@FreeBSD.org
Subject:   git: be711ade6f66 - main - nuageinit: implement MIME multipart user-data support
Message-ID:  <6a23bab9.26af4.37e5a0c6@gitrepo.freebsd.org>

index | next in thread | raw e-mail

The branch main has been updated by bapt:

URL: https://cgit.FreeBSD.org/src/commit/?id=be711ade6f66506fb2cae9fd33b142ce910f0346

commit be711ade6f66506fb2cae9fd33b142ce910f0346
Author:     Baptiste Daroussin <bapt@FreeBSD.org>
AuthorDate: 2026-06-05 20:45:54 +0000
Commit:     Baptiste Daroussin <bapt@FreeBSD.org>
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
 }


home | help

Want to link to this message? Use this
URL: <https://mail-archive.FreeBSD.org/cgi/mid.cgi?6a23bab9.26af4.37e5a0c6>