From nobody Sat Jun 6 06:14:17 2026 X-Original-To: dev-commits-src-main@mlmmj.nyi.freebsd.org Received: from mx1.freebsd.org (mx1.freebsd.org [IPv6:2610:1c1:1:606c::19:1]) by mlmmj.nyi.freebsd.org (Postfix) with ESMTP id 4gXSgY3fYPz6g4s0 for ; Sat, 06 Jun 2026 06:14:17 +0000 (UTC) (envelope-from git@FreeBSD.org) Received: from mxrelay.nyi.freebsd.org (mxrelay.nyi.freebsd.org [IPv6:2610:1c1:1:606c::19:3]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (4096 bits) server-digest SHA256 client-signature RSA-PSS (4096 bits) client-digest SHA256) (Client CN "mxrelay.nyi.freebsd.org", Issuer "R13" (not verified)) by mx1.freebsd.org (Postfix) with ESMTPS id 4gXSgY0bDvz3T8d for ; Sat, 06 Jun 2026 06:14:17 +0000 (UTC) (envelope-from git@FreeBSD.org) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=freebsd.org; s=dkim; t=1780726457; h=from:from:reply-to:subject:subject:date:date:message-id:message-id: to:to:cc:mime-version:mime-version:content-type:content-type: content-transfer-encoding:content-transfer-encoding; bh=vKipYYYp8QUPprKgIyf5jm8kI+57CIXhnpIpFLRDi40=; b=Cut10onpN5y7fnRoRf6ojMI65+lLhoOKVao+m+NsGtyeGGjfayN85oF99pEGhJYQMWfKJV 9EoaOFTsscATBiq1c2vzxxP4SfTJvG/wofW2/IkUu6GxJYXUb+FPJqAfSZ7117m5DoVzGi eZeY77YLv897LGv1gX37Sbm2oxiSCfMxovJsbnKnZ2C81JAv0/fqDSOzDuoKVw0SJ/65XT mnhBDV92aDU/31iVFQNDInRGNW59NJm02V7AnNm6uwQXCIA8d4s4neP4NdGVkbEm5v5lpe cQczjIMBcVKetunZ49gv33qxwLR3OgDUApyXDJIQozxI9hKRmzWD5EhMPGBsLQ== ARC-Seal: i=1; s=dkim; d=freebsd.org; t=1780726457; a=rsa-sha256; cv=none; b=hYOzig5HsMZma17l2rCHlIegzXZkspgYdJgmtABsjCpfV/7Vbqa+vk2Em//aDqCNCCM5jU GKJNA/PqaYcGE1MOX/sSsArG22Y9WIOYTf0WmFoXc4g0bdsxdf5avH0v1FymSl8Ap821gC laO0oq/p6N5GpqoAxzzgBPh7Nj16dkLFL6g5Z2QZ9/4G7vDcfxTFb3sbXR831SjzDMO5/n D8B2KPi7cH+NPL/0ok1QPS6wa5qVGigFACTYNkupU5ig8tlcqITCpUdNeSRvwD1jx24DGA nITD5U6zcTCgvgYxxu4gKEByL/U8KXDqv7DAImEH94XMNDB5R8BFqs9oYDWWOw== ARC-Authentication-Results: i=1; mx1.freebsd.org; none ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=freebsd.org; s=dkim; t=1780726457; h=from:from:reply-to:subject:subject:date:date:message-id:message-id: to:to:cc:mime-version:mime-version:content-type:content-type: content-transfer-encoding:content-transfer-encoding; bh=vKipYYYp8QUPprKgIyf5jm8kI+57CIXhnpIpFLRDi40=; b=wvqwzYyTj7dMLWXQg8V72qzgOVNtuu4YQmmAqrUVgEuQbFgS4WW8nZArwAeNhX60ErPHZl hN3jNKh5mpgMYEvpFIPr9CB5yZnZAa/nDNicgHhUJXF7gW5Y91jfg01NmUaW0gBj43c2b0 a1CXjttA1IBSA5AE8T1haVOpiiTrOlalCrfuuj22QeqyJbcG7HUTCW579+BG5kUFrL7UHW jD7gjJ9fMtTlLWGlvPfaW/iqfSFD/IM6XUmG6HgWdg9CWhvtq/+OGkTpIaJ8Bx2tRpqlc2 /hDwncEZ1gUw/d5r3LA+tyQ2wnZDwCMMnvgNY3sR0ghqdr3ReFxWOw1YTxp/Xw== Received: from gitrepo.freebsd.org (gitrepo.freebsd.org [IPv6:2610:1c1:1:6068::e6a:5]) by mxrelay.nyi.freebsd.org (Postfix) with ESMTP id 4gXSgY06zpz13xc for ; Sat, 06 Jun 2026 06:14:17 +0000 (UTC) (envelope-from git@FreeBSD.org) Received: from git (uid 1279) (envelope-from git@FreeBSD.org) id 26af4 by gitrepo.freebsd.org (DragonFly Mail Agent v0.13+ on gitrepo.freebsd.org); Sat, 06 Jun 2026 06:14:17 +0000 To: src-committers@FreeBSD.org, dev-commits-src-all@FreeBSD.org, dev-commits-src-main@FreeBSD.org From: Baptiste Daroussin Subject: git: be711ade6f66 - main - nuageinit: implement MIME multipart user-data support List-Id: Commit messages for the main branch of the src repository List-Archive: https://lists.freebsd.org/archives/dev-commits-src-main List-Help: List-Post: List-Subscribe: List-Unsubscribe: X-BeenThere: dev-commits-src-main@freebsd.org Sender: owner-dev-commits-src-main@FreeBSD.org List-Id: List-Post: List-Help: List-Subscribe: List-Unsubscribe: List-Owner: Precedence: list MIME-Version: 1.0 Content-Type: text/plain; charset=utf-8 Content-Transfer-Encoding: 8bit X-Git-Committer: bapt X-Git-Repository: src X-Git-Refname: refs/heads/main X-Git-Reftype: branch X-Git-Commit: be711ade6f66506fb2cae9fd33b142ce910f0346 Auto-Submitted: auto-generated Date: Sat, 06 Jun 2026 06:14:17 +0000 Message-Id: <6a23bab9.26af4.37e5a0c6@gitrepo.freebsd.org> The branch main has been updated by bapt: URL: https://cgit.FreeBSD.org/src/commit/?id=be711ade6f66506fb2cae9fd33b142ce910f0346 commit be711ade6f66506fb2cae9fd33b142ce910f0346 Author: Baptiste Daroussin AuthorDate: 2026-06-05 20:45:54 +0000 Commit: Baptiste Daroussin 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 }