diff --git a/internal/unifiedresources/adapters.go b/internal/unifiedresources/adapters.go index 86edfc1363..2f540df3a7 100644 --- a/internal/unifiedresources/adapters.go +++ b/internal/unifiedresources/adapters.go @@ -93,7 +93,10 @@ func resourceFromProxmoxNode(node models.Node, linkedHost *models.Host) (Resourc func identityFromHost(host models.Host) ResourceIdentity { ips, macs := collectInterfaceIDs(host.NetworkInterfaces) if host.ReportIP != "" { - ips = append(ips, host.ReportIP) + // --report-ip is the user naming the primary address on a multi-NIC + // host, and consumers treat the first entry as primary, so it must + // lead the list rather than trail the auto-detected ones (#829). + ips = append([]string{host.ReportIP}, ips...) } return ResourceIdentity{ diff --git a/internal/unifiedresources/adapters_test.go b/internal/unifiedresources/adapters_test.go index c15e732791..555bc82024 100644 --- a/internal/unifiedresources/adapters_test.go +++ b/internal/unifiedresources/adapters_test.go @@ -1752,6 +1752,22 @@ func TestCollectInterfaceIDsOrdersPhysicalInterfacesFirst(t *testing.T) { } } +func TestIdentityFromHostReportIPLeadsAddresses(t *testing.T) { + host := models.Host{ + Hostname: "multi-nic", + ReportIP: "192.168.1.9", + NetworkInterfaces: []models.HostNetworkInterface{ + {Name: "eth0", Addresses: []string{"2.2.2.10/24"}}, + {Name: "eth1", Addresses: []string{"192.168.1.9/24"}}, + }, + } + + identity := identityFromHost(host) + if len(identity.IPAddresses) == 0 || identity.IPAddresses[0] != "192.168.1.9" { + t.Fatalf("expected --report-ip to lead identity addresses, got %v", identity.IPAddresses) + } +} + func TestCollectInterfaceIDsAllVirtualStillCollects(t *testing.T) { interfaces := []models.HostNetworkInterface{ {Name: "docker0", Addresses: []string{"172.17.0.1/16"}}, diff --git a/scripts/install.sh b/scripts/install.sh index 319bd6c204..3b09be39b1 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -83,6 +83,7 @@ SERVER_FINGERPRINT="${PULSE_SERVER_FINGERPRINT:-}" OBSERVERS_FILE="${PULSE_OBSERVERS_FILE:-}" AGENT_ID="" HOSTNAME_OVERRIDE="" +REPORT_IP="" ENABLE_COMMANDS="false" HEALTH_ADDR="${PULSE_HEALTH_ADDR:-}" HEALTH_ADDR_SET="false" @@ -331,6 +332,7 @@ Options: --enable-proxmox Force enable Proxmox integration --agent-id Custom agent identifier --hostname Override hostname reported to Pulse + --report-ip IP address to report to Pulse (for multi-NIC systems) --state-dir Override persistent state directory --disk-exclude Exclude mount point (repeatable) --insecure Skip TLS verification (auto-enabled for http:// URLs) @@ -1530,6 +1532,7 @@ build_exec_arg_items() { if [[ "$KUBE_INCLUDE_ALL_DEPLOYMENTS" == "true" ]]; then EXEC_ARG_ITEMS+=(--kube-include-all-deployments); fi if [[ -n "$AGENT_ID" ]]; then EXEC_ARG_ITEMS+=(--agent-id "$AGENT_ID"); fi if [[ -n "$HOSTNAME_OVERRIDE" ]]; then EXEC_ARG_ITEMS+=(--hostname "$HOSTNAME_OVERRIDE"); fi + if [[ -n "$REPORT_IP" ]]; then EXEC_ARG_ITEMS+=(--report-ip "$REPORT_IP"); fi if [[ -n "$STATE_DIR" ]]; then EXEC_ARG_ITEMS+=(--state-dir "$STATE_DIR"); fi # Add disk exclude patterns (use ${arr[@]+"${arr[@]}"} for bash 3.2 compatibility with set -u) for pattern in ${DISK_EXCLUDES[@]+"${DISK_EXCLUDES[@]}"}; do @@ -1749,6 +1752,9 @@ recover_connection_state() { if [[ -z "$HOSTNAME_OVERRIDE" ]]; then HOSTNAME_OVERRIDE=$(read_connection_state_value "$file" "PULSE_HOSTNAME") fi + if [[ -z "$REPORT_IP" ]]; then + REPORT_IP=$(read_connection_state_value "$file" "PULSE_REPORT_IP") + fi if [[ "$INSECURE" != "true" ]]; then local saved_insecure="" saved_insecure=$(read_connection_state_value "$file" "PULSE_INSECURE_SKIP_VERIFY") @@ -1817,6 +1823,10 @@ apply_recovered_agent_arg_value() { if [[ -z "$HOSTNAME_OVERRIDE" ]]; then HOSTNAME_OVERRIDE="$value"; fi RECOVERED_AGENT_ARG_STATE="true" ;; + report-ip) + if [[ -z "$REPORT_IP" ]]; then REPORT_IP="$value"; fi + RECOVERED_AGENT_ARG_STATE="true" + ;; cacert) if [[ -z "$CURL_CA_BUNDLE" ]]; then CURL_CA_BUNDLE="$value"; fi RECOVERED_AGENT_ARG_STATE="true" @@ -1888,10 +1898,10 @@ recover_connection_state_from_arg_stream() { fi case "$arg" in - --url|--pulse-url|--token|--token-file|--interval|--agent-id|--hostname|--cacert|--server-fingerprint|--observers-file|--health-addr|--state-dir|--kubeconfig|--proxmox-type|--disk-exclude|-url|-pulse-url|-token|-token-file|-interval|-agent-id|-hostname|-cacert|-server-fingerprint|-observers-file|-health-addr|-state-dir|-kubeconfig|-proxmox-type|-disk-exclude) + --url|--pulse-url|--token|--token-file|--interval|--agent-id|--hostname|--report-ip|--cacert|--server-fingerprint|--observers-file|--health-addr|--state-dir|--kubeconfig|--proxmox-type|--disk-exclude|-url|-pulse-url|-token|-token-file|-interval|-agent-id|-hostname|-report-ip|-cacert|-server-fingerprint|-observers-file|-health-addr|-state-dir|-kubeconfig|-proxmox-type|-disk-exclude) pending_key=$(normalize_recovered_agent_arg_key "$arg") ;; - --url=*|--pulse-url=*|--token=*|--token-file=*|--interval=*|--agent-id=*|--hostname=*|--cacert=*|--server-fingerprint=*|--observers-file=*|--health-addr=*|--state-dir=*|--kubeconfig=*|--proxmox-type=*|--disk-exclude=*|-url=*|-pulse-url=*|-token=*|-token-file=*|-interval=*|-agent-id=*|-hostname=*|-cacert=*|-server-fingerprint=*|-observers-file=*|-health-addr=*|-state-dir=*|-kubeconfig=*|-proxmox-type=*|-disk-exclude=*) + --url=*|--pulse-url=*|--token=*|--token-file=*|--interval=*|--agent-id=*|--hostname=*|--report-ip=*|--cacert=*|--server-fingerprint=*|--observers-file=*|--health-addr=*|--state-dir=*|--kubeconfig=*|--proxmox-type=*|--disk-exclude=*|-url=*|-pulse-url=*|-token=*|-token-file=*|-interval=*|-agent-id=*|-hostname=*|-report-ip=*|-cacert=*|-server-fingerprint=*|-observers-file=*|-health-addr=*|-state-dir=*|-kubeconfig=*|-proxmox-type=*|-disk-exclude=*) key="${arg%%=*}" value="${arg#*=}" apply_recovered_agent_arg_value "$key" "$value" @@ -2021,6 +2031,11 @@ recover_connection_state_from_env_stream() { if [[ -z "$HOSTNAME_OVERRIDE" ]]; then HOSTNAME_OVERRIDE="$value"; fi RECOVERED_AGENT_ENV_STATE="true" ;; + PULSE_REPORT_IP=*) + value="${env_line#*=}" + if [[ -z "$REPORT_IP" ]]; then REPORT_IP="$value"; fi + RECOVERED_AGENT_ENV_STATE="true" + ;; PULSE_INSECURE_SKIP_VERIFY=true) INSECURE="true" RECOVERED_AGENT_ENV_STATE="true" @@ -2409,6 +2424,7 @@ save_connection_info() { write_connection_state_value "$conn_tmp" "PULSE_TOKEN_FILE" "$RUNTIME_TOKEN_FILE" write_connection_state_value "$conn_tmp" "PULSE_AGENT_ID" "$AGENT_ID" write_connection_state_value "$conn_tmp" "PULSE_HOSTNAME" "$HOSTNAME_OVERRIDE" + write_connection_state_value "$conn_tmp" "PULSE_REPORT_IP" "$REPORT_IP" if [[ "$INSECURE" == "true" ]]; then write_connection_state_value "$conn_tmp" "PULSE_INSECURE_SKIP_VERIFY" "true" fi @@ -2477,6 +2493,7 @@ while [[ $# -gt 0 ]]; do --uninstall) UNINSTALL="true"; shift ;; --agent-id) AGENT_ID="$2"; shift 2 ;; --hostname) HOSTNAME_OVERRIDE="$2"; shift 2 ;; + --report-ip) REPORT_IP="$2"; shift 2 ;; --state-dir) STATE_DIR="$2"; STATE_DIR_SOURCE="explicit"; shift 2 ;; --kube-include-all-pods) KUBE_INCLUDE_ALL_PODS="true"; shift ;; --kube-include-all-deployments) KUBE_INCLUDE_ALL_DEPLOYMENTS="true"; shift ;; diff --git a/scripts/installtests/agent_state_dir_lifecycle_test.go b/scripts/installtests/agent_state_dir_lifecycle_test.go index b0782cbb0a..20da721536 100644 --- a/scripts/installtests/agent_state_dir_lifecycle_test.go +++ b/scripts/installtests/agent_state_dir_lifecycle_test.go @@ -178,6 +178,7 @@ func renderLifecycleService(t *testing.T, stateDir, stateSource, unitPath, pulse PULSE_TOKEN="" AGENT_ID="" HOSTNAME_OVERRIDE="" + REPORT_IP="" INSECURE="false" SERVER_FINGERPRINT="" CURL_CA_BUNDLE="" @@ -211,6 +212,7 @@ func renderLifecycleService(t *testing.T, stateDir, stateSource, unitPath, pulse KUBE_INCLUDE_ALL_DEPLOYMENTS="false" AGENT_ID="" HOSTNAME_OVERRIDE="state-lifecycle-host" + REPORT_IP="" DISK_EXCLUDES=() CURL_CA_BUNDLE="" RUNTIME_TOKEN_FILE="" diff --git a/scripts/installtests/install_sh_test.go b/scripts/installtests/install_sh_test.go index 03af150aaf..1e532db882 100644 --- a/scripts/installtests/install_sh_test.go +++ b/scripts/installtests/install_sh_test.go @@ -716,7 +716,7 @@ func TestInstallSHSupportsSavedStateUpdateMode(t *testing.T) { `recover_connection_state_from_arg_stream`, `recover_token_from_default_agent_token_file() {`, `normalize_recovered_agent_arg_key() {`, - `-url|-pulse-url|-token|-token-file|-interval|-agent-id|-hostname|-cacert|-server-fingerprint|-observers-file|-health-addr|-state-dir|-kubeconfig|-proxmox-type|-disk-exclude)`, + `-url|-pulse-url|-token|-token-file|-interval|-agent-id|-hostname|-report-ip|-cacert|-server-fingerprint|-observers-file|-health-addr|-state-dir|-kubeconfig|-proxmox-type|-disk-exclude)`, `--enable-host|-enable-host|--enable-host=true|-enable-host=true)`, `recover_connection_state_from_env_stream`, `recovered_connection_state_ready() {`, @@ -2582,6 +2582,7 @@ func TestInstallSHConnectionEnvPersistsCanonicalStateDirWithoutTokenValue(t *tes RUNTIME_TOKEN_FILE="$STATE_DIR/token" AGENT_ID="agent-custom" HOSTNAME_OVERRIDE="host-custom" + REPORT_IP="192.168.1.9" INSECURE="false" SERVER_FINGERPRINT="" CURL_CA_BUNDLE="" @@ -2601,6 +2602,9 @@ func TestInstallSHConnectionEnvPersistsCanonicalStateDirWithoutTokenValue(t *tes !strings.Contains(got, "PULSE_TOKEN_FILE='"+filepath.Join(stateDir, "token")+"'") { t.Fatalf("connection.env did not persist canonical state paths:\n%s", got) } + if !strings.Contains(got, "PULSE_REPORT_IP='192.168.1.9'") { + t.Fatalf("connection.env did not persist the report IP:\n%s", got) + } if strings.Contains(got, "PULSE_TOKEN='") || strings.Contains(got, "deadbeef") { t.Fatalf("connection.env leaked the token value:\n%s", got) } @@ -2643,6 +2647,7 @@ func TestInstallSHStateWritesReplaceSymlinksAtomically(t *testing.T) { ENROLL="false" AGENT_ID="agent-one" HOSTNAME_OVERRIDE="host-one" + REPORT_IP="" INSECURE="false" SERVER_FINGERPRINT="" CURL_CA_BUNDLE=""