package store import ( "encoding/json" "net" "net/url" "strings" ) // SpeakerMeta holds well-known keys from bgp_speaker.meta_json. type SpeakerMeta struct { AgentDomain string `json:"agent_domain,omitempty"` AgentSecret string `json:"agent_secret,omitempty"` AgentPort int `json:"agent_port,omitempty"` NodeIPv4 string `json:"node_ipv4,omitempty"` BirdBgpSourceIPv4 string `json:"bird_bgp_source_ipv4,omitempty"` BirdBgpSourceIPv6 string `json:"bird_bgp_source_ipv6,omitempty"` NodeEnrolledAt string `json:"node_enrolled_at,omitempty"` LastDispatchAt string `json:"last_dispatch_at,omitempty"` LastDispatchError string `json:"last_dispatch_error,omitempty"` LastDispatchStatus string `json:"last_dispatch_status,omitempty"` SyncStatus string `json:"sync_status,omitempty"` FirewallFailover bool `json:"firewall_failover,omitempty"` LastFirewallReplicateAt string `json:"last_firewall_replicate_at,omitempty"` LastFirewallReplicateStatus string `json:"last_firewall_replicate_status,omitempty"` LastFirewallReplicateError string `json:"last_firewall_replicate_error,omitempty"` } // ParseSpeakerMeta decodes meta_json object; unknown keys are ignored. func ParseSpeakerMeta(metaJSON string) SpeakerMeta { raw := strings.TrimSpace(metaJSON) if raw == "" || raw == "{}" { return SpeakerMeta{} } var m SpeakerMeta _ = json.Unmarshal([]byte(raw), &m) if m.AgentPort == 0 { m.AgentPort = 8443 } return m } // SpeakerMetaJSON marshals SpeakerMeta to a JSON object string. func SpeakerMetaJSON(m SpeakerMeta) string { b, err := json.Marshal(m) if err != nil { return "{}" } return string(b) } // MergeSpeakerMetaJSON merges patch into existing meta_json string. func MergeSpeakerMetaJSON(existing string, patch SpeakerMeta) string { cur := ParseSpeakerMeta(existing) if patch.AgentDomain != "" { cur.AgentDomain = patch.AgentDomain } if patch.AgentSecret != "" { cur.AgentSecret = patch.AgentSecret } if patch.AgentPort != 0 { cur.AgentPort = patch.AgentPort } if patch.NodeIPv4 != "" { cur.NodeIPv4 = patch.NodeIPv4 } if patch.BirdBgpSourceIPv4 != "" { cur.BirdBgpSourceIPv4 = patch.BirdBgpSourceIPv4 } if patch.BirdBgpSourceIPv6 != "" { cur.BirdBgpSourceIPv6 = patch.BirdBgpSourceIPv6 } if patch.NodeEnrolledAt != "" { cur.NodeEnrolledAt = patch.NodeEnrolledAt } if patch.LastDispatchAt != "" { cur.LastDispatchAt = patch.LastDispatchAt } if patch.LastDispatchStatus == "ok" { cur.LastDispatchError = "" } else if patch.LastDispatchError != "" { cur.LastDispatchError = patch.LastDispatchError } if patch.LastDispatchStatus != "" { cur.LastDispatchStatus = patch.LastDispatchStatus } if patch.SyncStatus != "" { cur.SyncStatus = patch.SyncStatus } if patch.FirewallFailover { cur.FirewallFailover = true } if patch.LastFirewallReplicateAt != "" { cur.LastFirewallReplicateAt = patch.LastFirewallReplicateAt } if patch.LastFirewallReplicateStatus == "ok" { cur.LastFirewallReplicateError = "" } else if patch.LastFirewallReplicateError != "" { cur.LastFirewallReplicateError = patch.LastFirewallReplicateError } if patch.LastFirewallReplicateStatus != "" { cur.LastFirewallReplicateStatus = patch.LastFirewallReplicateStatus } return SpeakerMetaJSON(cur) } // IPv4FromEndpoint extracts an IPv4 from endpoint URL host when present. func IPv4FromEndpoint(endpoint string) string { ep := strings.TrimSpace(endpoint) if ep == "" { return "" } if !strings.Contains(ep, "://") { ep = "https://" + ep } u, err := url.Parse(ep) if err != nil { return "" } host := strings.TrimSpace(u.Hostname()) if host == "" { return "" } if ip := net.ParseIP(host); ip != nil && ip.To4() != nil { return ip.String() } return "" } // ValidIPv4 reports whether s is a dotted-quad IPv4 address. func ValidIPv4(s string) bool { ip := net.ParseIP(strings.TrimSpace(s)) return ip != nil && ip.To4() != nil } // AgentSyncURL returns HTTPS sync URL for a speaker with agent_domain configured. func AgentSyncURL(meta SpeakerMeta) string { domain := strings.TrimSpace(meta.AgentDomain) if domain == "" { return "" } return "https://" + strings.TrimSuffix(domain, "/") + "/v1/agent/sync" } // AgentHealthURL returns HTTPS health URL for agent_domain. func AgentHealthURL(meta SpeakerMeta) string { return agentHTTPSURL(meta, "/v1/agent/health") } // AgentBirdProtocolsURL returns HTTPS bird protocols URL for agent_domain. func AgentBirdProtocolsURL(meta SpeakerMeta) string { return agentHTTPSURL(meta, "/v1/agent/bird/protocols") } func agentHTTPSURL(meta SpeakerMeta, path string) string { domain := strings.TrimSpace(meta.AgentDomain) if domain == "" { return "" } return "https://" + strings.TrimSuffix(domain, "/") + path } // SpeakerNeedsRemoteDispatch reports whether deploy_apply should wake this speaker via agent HTTP. func SpeakerNeedsRemoteDispatch(role string, meta SpeakerMeta) bool { r := strings.ToLower(strings.TrimSpace(role)) if r == "master" { return false } return strings.TrimSpace(meta.AgentDomain) != "" && strings.TrimSpace(meta.AgentSecret) != "" }