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.
This commit is contained in:
Baptiste Daroussin
2026-06-05 22:45:54 +02:00
parent b56f029add
commit be711ade6f
4 changed files with 132 additions and 0 deletions
+45
View File
@@ -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
+38
View File
@@ -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
+14
View File
@@ -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 :
+35
View File
@@ -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
}