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
 }

Reply via email to