git: 58653bf4d0fb - main - nuageinit: implement phone_home support

From: Baptiste Daroussin <bapt_at_FreeBSD.org>
Date: Sat, 06 Jun 2026 06:14:21 UTC
The branch main has been updated by bapt:

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

commit 58653bf4d0fb8ccd5de146d671ec101a1df0ede0
Author:     Baptiste Daroussin <bapt@FreeBSD.org>
AuthorDate: 2026-06-05 21:28:25 +0000
Commit:     Baptiste Daroussin <bapt@FreeBSD.org>
CommitDate: 2026-06-05 21:28:25 +0000

    nuageinit: implement phone_home support
    
    Posts instance data (hostname, instance_id, public keys) to a URL
    using fetch(1). Supports:
    - url: target URL
    - post: list of data items to send, or 'all'
    - tries: number of retry attempts (default 1)
---
 libexec/nuageinit/nuageinit          | 91 +++++++++++++++++++++++++++++++++++-
 libexec/nuageinit/tests/nuageinit.sh | 33 +++++++++++++
 2 files changed, 122 insertions(+), 2 deletions(-)

diff --git a/libexec/nuageinit/nuageinit b/libexec/nuageinit/nuageinit
index 1391aff13bd6..8e207cae0a15 100755
--- a/libexec/nuageinit/nuageinit
+++ b/libexec/nuageinit/nuageinit
@@ -808,6 +808,79 @@ local function final_message(obj)
 	nuage.warn(msg, false)
 end
 
+local function phone_home(obj, metadata)
+	if obj.phone_home == nil then return end
+	local ph = obj.phone_home
+	if type(ph) ~= "table" then
+		nuage.warn("phone_home must be an object")
+		return
+	end
+	if not ph.url then
+		nuage.warn("phone_home.url is required")
+		return
+	end
+	local url = ph.url
+	local tries = ph.tries or 1
+	if type(tries) ~= "number" or tries < 1 then
+		tries = 1
+	end
+
+	-- Collect data to post
+	local data = {}
+	local post = ph.post
+	if post == "all" then
+		post = {"pub_key_rsa", "pub_key_ecdsa", "pub_key_ed25519",
+		    "instance_id", "hostname", "fqdn"}
+	end
+	if type(post) == "table" then
+		for _, key in ipairs(post) do
+			if key == "hostname" then
+				if metadata.hostname then
+					table.insert(data, "hostname=" .. metadata.hostname)
+				end
+			elseif key == "fqdn" then
+				if metadata.hostname then
+					table.insert(data, "fqdn=" .. metadata.hostname)
+				end
+			elseif key == "instance_id" then
+				if metadata.uuid then
+					table.insert(data, "instance_id=" .. metadata.uuid)
+				end
+			elseif key:match("^pub_key_") then
+				local algo = key:match("^pub_key_(.+)$")
+				if metadata.public_keys then
+					for _, k in ipairs(metadata.public_keys) do
+						if algo == "rsa" and k:match("^ssh%-rsa ") then
+							table.insert(data, key .. "=" .. nuage.encode_base64(k))
+						elseif algo == "ecdsa" and k:match("^ecdsa%-") then
+							table.insert(data, key .. "=" .. nuage.encode_base64(k))
+						elseif algo == "ed25519" and k:match("^ssh%-ed25519 ") then
+							table.insert(data, key .. "=" .. nuage.encode_base64(k))
+						end
+					end
+				end
+			end
+		end
+	end
+	local post_data = table.concat(data, "&")
+	local cmd = "fetch -q -o /dev/null --post-data " .. nuage.shell_escape(post_data)
+	cmd = cmd .. " " .. nuage.shell_escape(url)
+
+	if os.getenv("NUAGE_RUN_TESTS") then
+		print(cmd)
+		return
+	end
+	for i = 1, tries do
+		if os.execute(cmd) then
+			break
+		end
+		if i < tries then
+			-- wait 1 second before retrying
+			os.execute("sleep 1")
+		end
+	end
+end
+
 local function chpasswd(obj)
 	if obj.chpasswd == nil then return end
 	nuage.chpasswd(obj.chpasswd)
@@ -973,10 +1046,23 @@ local function load_metadata(citype)
 			nuage.err("error parsing nocloud meta-data")
 		end
 		return obj
-	elseif citype ~= "postnet" then
+	elseif citype == "postnet" then
+		-- reload metadata: try config-2 format first, then nocloud
+		local parser = ucl.parser()
+		local res, err = parser:parse_file(ni_path .. "/meta_data.json")
+		if res then
+			return parser:get_object()
+		end
+		local f = io.open(ni_path .. "/meta-data")
+		if f then
+			local obj = yaml.load(f:read("*a"))
+			f:close()
+			if obj then return obj end
+		end
+		return {}
+	else
 		nuage.err("Unknown cloud init type: " .. citype)
 	end
-	return {}
 end
 
 local function load_userdata()
@@ -1114,6 +1200,7 @@ elseif line == "#cloud-config" then
 		users,
 		chpasswd,
 		write_files_deferred,
+		phone_home,
 		final_message,
 		power_state_change,
 	}
diff --git a/libexec/nuageinit/tests/nuageinit.sh b/libexec/nuageinit/tests/nuageinit.sh
index 2b4d316fd5ff..b225289718e6 100644
--- a/libexec/nuageinit/tests/nuageinit.sh
+++ b/libexec/nuageinit/tests/nuageinit.sh
@@ -47,6 +47,7 @@ atf_test_case config2_userdata_fqdn_and_hostname
 atf_test_case config2_userdata_write_files
 atf_test_case config2_userdata_encode_base64
 atf_test_case config2_userdata_final_message
+atf_test_case config2_userdata_phone_home
 
 setup_test_adduser()
 {
@@ -1416,6 +1417,37 @@ EOF
 	    /usr/libexec/nuageinit "${PWD}"/media/nuageinit postnet
 }
 
+config2_userdata_phone_home_body()
+{
+	mkdir -p media/nuageinit
+	setup_test_adduser
+	export NUAGE_RUN_TESTS=1
+	printf '{"hostname": "myhost", "uuid": "abc-123", "public_keys": ["ssh-rsa AAAAB...", "ssh-ed25519 AAAAC..."]}' > media/nuageinit/meta_data.json
+	cat > media/nuageinit/user_data << 'EOF'
+#cloud-config
+phone_home:
+  url: "http://example.com/endpoint"
+  post:
+    - hostname
+    - instance_id
+  tries: 1
+EOF
+	atf_check -o match:"fetch -q -o /dev/null --post-data 'hostname=myhost&instance_id=abc-123' 'http://example.com/endpoint'" \
+	    /usr/libexec/nuageinit "${PWD}"/media/nuageinit postnet
+
+	# Test "all" post
+	printf '{"hostname": "myhost"}' > media/nuageinit/meta_data.json
+	cat > media/nuageinit/user_data << 'EOF'
+#cloud-config
+phone_home:
+  url: "http://example.com/endpoint"
+  post: all
+  tries: 1
+EOF
+	atf_check -o match:"fetch -q -o /dev/null --post-data 'hostname=myhost&fqdn=myhost' 'http://example.com/endpoint'" \
+	    /usr/libexec/nuageinit "${PWD}"/media/nuageinit postnet
+}
+
 atf_init_test_cases()
 {
 	atf_add_test_case args
@@ -1458,4 +1490,5 @@ atf_init_test_cases()
 	atf_add_test_case config2_userdata_write_files
 	atf_add_test_case config2_userdata_encode_base64
 	atf_add_test_case config2_userdata_final_message
+	atf_add_test_case config2_userdata_phone_home
 }