From 7fbbb6d0f7d5131f84b04f4db15090bfe1e1006c Mon Sep 17 00:00:00 2001 From: Sergey Antropoff Date: Mon, 15 Sep 2025 17:28:07 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=BE=20=D0=BE=D0=B1=D0=BD=D0=B0=D1=80=D1=83=D0=B6?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D0=B5=2018=20=D0=BD=D0=BE=D0=B2=D1=8B=D1=85?= =?UTF-8?q?=20=D1=81=D0=B5=D1=80=D0=B2=D0=B8=D1=81=D0=BE=D0=B2=20=D0=B2=20?= =?UTF-8?q?=D0=BA=D0=BE=D0=BB=D0=BB=D0=B5=D0=BA=D1=82=D0=BE=D1=80=20proxvm?= =?UTF-8?q?services?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Добавлены следующие сервисы с поддержкой кластеров: **Standalone сервисы:** - BIND9 DNS сервер (порты: 53, 953) - Grafana (порт: 3000) - Prometheus (порт: 9090) - Loki (порт: 3100) - Harbor (порты: 80, 443, 8080) - Jenkins (порты: 8080, 50000) - Keycloak (порты: 8080, 8443) - Neo4j (порты: 7474, 7687) - Sentry (порты: 9000, 9001) - Apache Superset (порт: 8088) - InfluxDB (порт: 8086) - VictoriaMetrics (порты: 8428, 8429) **Кластерные сервисы:** - DragonflyDB (порты: 6379, 6380) - через dragonfly cluster nodes - Elasticsearch (порты: 9200, 9300) - через HTTP API /_cluster/state/nodes - Greenplum (порты: 5432, 28080) - через gpstate -s - MinIO (порты: 9000, 9001) - через mc admin info - Redpanda (порты: 9092, 9644) - через rpk cluster info - NATS (порты: 4222, 8222) - через nats server list **Особенности реализации:** - Все сервисы определяют тип (standalone/cluster) автоматически - Кластерные сервисы извлекают IP всех нод кластера - Поддержка получения версий через CLI и HTTP API - Fallback на localhost для standalone сервисов - Обработка ошибок при недоступности команд управления кластерами **Результаты тестирования:** - Proxmox нода: обнаружено 8 сервисов (PostgreSQL, etcd, MongoDB, Elasticsearch, Grafana, Harbor, Keycloak, Superset) - VM: обнаружено 2 сервиса (Kubernetes, Prometheus) - LXC: обнаружен 1 сервис (PostgreSQL 11.17) Автор: Сергей Антропов Сайт: https://devops.org.ru --- .../proxvmservices/proxvmservices_linux.go | 843 ++++++++++++++++++ 1 file changed, 843 insertions(+) diff --git a/src/collectors/proxvmservices/proxvmservices_linux.go b/src/collectors/proxvmservices/proxvmservices_linux.go index 797b4ed..35fa94d 100644 --- a/src/collectors/proxvmservices/proxvmservices_linux.go +++ b/src/collectors/proxvmservices/proxvmservices_linux.go @@ -117,6 +117,62 @@ func collectProxVMServices(ctx context.Context) (map[string]any, error) { if k8s := detectKubernetes(); k8s != nil { services = append(services, *k8s) } + + // Новые сервисы + if bind9 := detectBind9(); bind9 != nil { + services = append(services, *bind9) + } + if dragonfly := detectDragonflyDB(); dragonfly != nil { + services = append(services, *dragonfly) + } + if elasticsearch := detectElasticsearch(); elasticsearch != nil { + services = append(services, *elasticsearch) + } + if grafana := detectGrafana(); grafana != nil { + services = append(services, *grafana) + } + if prometheus := detectPrometheus(); prometheus != nil { + services = append(services, *prometheus) + } + if loki := detectLoki(); loki != nil { + services = append(services, *loki) + } + if greenplum := detectGreenplum(); greenplum != nil { + services = append(services, *greenplum) + } + if harbor := detectHarbor(); harbor != nil { + services = append(services, *harbor) + } + if jenkins := detectJenkins(); jenkins != nil { + services = append(services, *jenkins) + } + if keycloak := detectKeycloak(); keycloak != nil { + services = append(services, *keycloak) + } + if minio := detectMinio(); minio != nil { + services = append(services, *minio) + } + if neo4j := detectNeo4j(); neo4j != nil { + services = append(services, *neo4j) + } + if redpanda := detectRedpanda(); redpanda != nil { + services = append(services, *redpanda) + } + if sentry := detectSentry(); sentry != nil { + services = append(services, *sentry) + } + if superset := detectSuperset(); superset != nil { + services = append(services, *superset) + } + if nats := detectNats(); nats != nil { + services = append(services, *nats) + } + if influxdb := detectInfluxDB(); influxdb != nil { + services = append(services, *influxdb) + } + if victoriametrics := detectVictoriaMetrics(); victoriametrics != nil { + services = append(services, *victoriametrics) + } result["services"] = services @@ -1174,3 +1230,790 @@ func detectKubernetes() *ServiceInfo { ClusterNodes: clusterNodes, } } + +// detectBind9 обнаруживает BIND9 DNS сервер +func detectBind9() *ServiceInfo { + if !isProcessRunning("named") && !isProcessRunning("bind9") { + return nil + } + + ports := getListeningPorts(53, 953) + version := "unknown" + + versionOutput, err := runCommand("named", "-v") + if err == nil { + re := regexp.MustCompile(`(\d+\.\d+\.\d+)`) + matches := re.FindStringSubmatch(versionOutput) + if len(matches) > 1 { + version = matches[1] + } + } + + return &ServiceInfo{ + Name: "bind9", + Type: "standalone", + Status: "running", + Version: version, + Ports: ports, + Config: make(map[string]any), + ClusterNodes: []string{"127.0.0.1"}, + } +} + +// detectDragonflyDB обнаруживает DragonflyDB +func detectDragonflyDB() *ServiceInfo { + if !isProcessRunning("dragonfly") { + return nil + } + + ports := getListeningPorts(6379, 6380) + version := "unknown" + + versionOutput, err := runCommand("dragonfly", "--version") + if err == nil { + re := regexp.MustCompile(`(\d+\.\d+\.\d+)`) + matches := re.FindStringSubmatch(versionOutput) + if len(matches) > 1 { + version = matches[1] + } + } + + // Получаем ноды DragonflyDB кластера + clusterNodes := getDragonflyDBClusterNodes() + serviceType := "standalone" + if len(clusterNodes) > 1 { + serviceType = "cluster" + } + + return &ServiceInfo{ + Name: "dragonflydb", + Type: serviceType, + Status: "running", + Version: version, + Ports: ports, + Config: make(map[string]any), + ClusterNodes: clusterNodes, + } +} + +// detectElasticsearch обнаруживает Elasticsearch +func detectElasticsearch() *ServiceInfo { + if !isProcessRunning("elasticsearch") && !isProcessRunning("java.*elasticsearch") { + return nil + } + + ports := getListeningPorts(9200, 9300) + version := "unknown" + + // Пробуем получить версию через HTTP API + versionOutput, err := runCommand("curl", "-s", "http://localhost:9200") + if err == nil { + re := regexp.MustCompile(`"number"\s*:\s*"([^"]+)"`) + matches := re.FindStringSubmatch(versionOutput) + if len(matches) > 1 { + version = matches[1] + } + } + + // Получаем ноды Elasticsearch кластера + clusterNodes := getElasticsearchClusterNodes() + serviceType := "standalone" + if len(clusterNodes) > 1 { + serviceType = "cluster" + } + + return &ServiceInfo{ + Name: "elasticsearch", + Type: serviceType, + Status: "running", + Version: version, + Ports: ports, + Config: make(map[string]any), + ClusterNodes: clusterNodes, + } +} + +// detectGrafana обнаруживает Grafana +func detectGrafana() *ServiceInfo { + if !isProcessRunning("grafana-server") && !isProcessRunning("grafana") { + return nil + } + + ports := getListeningPorts(3000) + version := "unknown" + + versionOutput, err := runCommand("grafana-server", "--version") + if err == nil { + re := regexp.MustCompile(`(\d+\.\d+\.\d+)`) + matches := re.FindStringSubmatch(versionOutput) + if len(matches) > 1 { + version = matches[1] + } + } + + return &ServiceInfo{ + Name: "grafana", + Type: "standalone", + Status: "running", + Version: version, + Ports: ports, + Config: make(map[string]any), + ClusterNodes: []string{"127.0.0.1"}, + } +} + +// detectPrometheus обнаруживает Prometheus +func detectPrometheus() *ServiceInfo { + if !isProcessRunning("prometheus") { + return nil + } + + ports := getListeningPorts(9090) + version := "unknown" + + versionOutput, err := runCommand("prometheus", "--version") + if err == nil { + re := regexp.MustCompile(`(\d+\.\d+\.\d+)`) + matches := re.FindStringSubmatch(versionOutput) + if len(matches) > 1 { + version = matches[1] + } + } + + return &ServiceInfo{ + Name: "prometheus", + Type: "standalone", + Status: "running", + Version: version, + Ports: ports, + Config: make(map[string]any), + ClusterNodes: []string{"127.0.0.1"}, + } +} + +// detectLoki обнаруживает Loki +func detectLoki() *ServiceInfo { + if !isProcessRunning("loki") { + return nil + } + + ports := getListeningPorts(3100) + version := "unknown" + + versionOutput, err := runCommand("loki", "--version") + if err == nil { + re := regexp.MustCompile(`(\d+\.\d+\.\d+)`) + matches := re.FindStringSubmatch(versionOutput) + if len(matches) > 1 { + version = matches[1] + } + } + + return &ServiceInfo{ + Name: "loki", + Type: "standalone", + Status: "running", + Version: version, + Ports: ports, + Config: make(map[string]any), + ClusterNodes: []string{"127.0.0.1"}, + } +} + +// detectGreenplum обнаруживает Greenplum +func detectGreenplum() *ServiceInfo { + if !isProcessRunning("postgres.*greenplum") && !isProcessRunning("gpdb") { + return nil + } + + ports := getListeningPorts(5432, 28080) + version := "unknown" + + versionOutput, err := runCommand("psql", "--version") + if err == nil { + re := regexp.MustCompile(`(\d+\.\d+\.\d+)`) + matches := re.FindStringSubmatch(versionOutput) + if len(matches) > 1 { + version = matches[1] + } + } + + // Получаем ноды Greenplum кластера + clusterNodes := getGreenplumClusterNodes() + serviceType := "standalone" + if len(clusterNodes) > 1 { + serviceType = "cluster" + } + + return &ServiceInfo{ + Name: "greenplum", + Type: serviceType, + Status: "running", + Version: version, + Ports: ports, + Config: make(map[string]any), + ClusterNodes: clusterNodes, + } +} + +// detectHarbor обнаруживает Harbor +func detectHarbor() *ServiceInfo { + if !isProcessRunning("harbor") && !isProcessRunning("nginx.*harbor") { + return nil + } + + ports := getListeningPorts(80, 443, 8080) + version := "unknown" + + // Пробуем получить версию через HTTP API + versionOutput, err := runCommand("curl", "-s", "http://localhost/api/v2.0/systeminfo") + if err == nil { + re := regexp.MustCompile(`"harbor_version"\s*:\s*"([^"]+)"`) + matches := re.FindStringSubmatch(versionOutput) + if len(matches) > 1 { + version = matches[1] + } + } + + return &ServiceInfo{ + Name: "harbor", + Type: "standalone", + Status: "running", + Version: version, + Ports: ports, + Config: make(map[string]any), + ClusterNodes: []string{"127.0.0.1"}, + } +} + +// detectJenkins обнаруживает Jenkins +func detectJenkins() *ServiceInfo { + if !isProcessRunning("jenkins") && !isProcessRunning("java.*jenkins") { + return nil + } + + ports := getListeningPorts(8080, 50000) + version := "unknown" + + // Пробуем получить версию через HTTP API + versionOutput, err := runCommand("curl", "-s", "http://localhost:8080/api/json") + if err == nil { + re := regexp.MustCompile(`"jenkins_version"\s*:\s*"([^"]+)"`) + matches := re.FindStringSubmatch(versionOutput) + if len(matches) > 1 { + version = matches[1] + } + } + + return &ServiceInfo{ + Name: "jenkins", + Type: "standalone", + Status: "running", + Version: version, + Ports: ports, + Config: make(map[string]any), + ClusterNodes: []string{"127.0.0.1"}, + } +} + +// detectKeycloak обнаруживает Keycloak +func detectKeycloak() *ServiceInfo { + if !isProcessRunning("keycloak") && !isProcessRunning("java.*keycloak") { + return nil + } + + ports := getListeningPorts(8080, 8443) + version := "unknown" + + // Пробуем получить версию через HTTP API + versionOutput, err := runCommand("curl", "-s", "http://localhost:8080/auth/admin/info") + if err == nil { + // Пробуем новый путь для Keycloak 17+ + versionOutput, err = runCommand("curl", "-s", "http://localhost:8080/admin/info") + } + if err == nil { + re := regexp.MustCompile(`"version"\s*:\s*"([^"]+)"`) + matches := re.FindStringSubmatch(versionOutput) + if len(matches) > 1 { + version = matches[1] + } + } + + return &ServiceInfo{ + Name: "keycloak", + Type: "standalone", + Status: "running", + Version: version, + Ports: ports, + Config: make(map[string]any), + ClusterNodes: []string{"127.0.0.1"}, + } +} + +// detectMinio обнаруживает MinIO +func detectMinio() *ServiceInfo { + if !isProcessRunning("minio") { + return nil + } + + ports := getListeningPorts(9000, 9001) + version := "unknown" + + versionOutput, err := runCommand("minio", "--version") + if err == nil { + re := regexp.MustCompile(`(\d+\.\d+\.\d+)`) + matches := re.FindStringSubmatch(versionOutput) + if len(matches) > 1 { + version = matches[1] + } + } + + // Получаем ноды MinIO кластера + clusterNodes := getMinioClusterNodes() + serviceType := "standalone" + if len(clusterNodes) > 1 { + serviceType = "cluster" + } + + return &ServiceInfo{ + Name: "minio", + Type: serviceType, + Status: "running", + Version: version, + Ports: ports, + Config: make(map[string]any), + ClusterNodes: clusterNodes, + } +} + +// detectNeo4j обнаруживает Neo4j +func detectNeo4j() *ServiceInfo { + if !isProcessRunning("neo4j") && !isProcessRunning("java.*neo4j") { + return nil + } + + ports := getListeningPorts(7474, 7687) + version := "unknown" + + // Пробуем получить версию через HTTP API + versionOutput, err := runCommand("curl", "-s", "http://localhost:7474/db/data/") + if err == nil { + re := regexp.MustCompile(`"neo4j_version"\s*:\s*"([^"]+)"`) + matches := re.FindStringSubmatch(versionOutput) + if len(matches) > 1 { + version = matches[1] + } + } + + return &ServiceInfo{ + Name: "neo4j", + Type: "standalone", + Status: "running", + Version: version, + Ports: ports, + Config: make(map[string]any), + ClusterNodes: []string{"127.0.0.1"}, + } +} + +// detectRedpanda обнаруживает Redpanda +func detectRedpanda() *ServiceInfo { + if !isProcessRunning("redpanda") { + return nil + } + + ports := getListeningPorts(9092, 9644) + version := "unknown" + + versionOutput, err := runCommand("rpk", "version") + if err == nil { + re := regexp.MustCompile(`(\d+\.\d+\.\d+)`) + matches := re.FindStringSubmatch(versionOutput) + if len(matches) > 1 { + version = matches[1] + } + } + + // Получаем ноды Redpanda кластера + clusterNodes := getRedpandaClusterNodes() + serviceType := "standalone" + if len(clusterNodes) > 1 { + serviceType = "cluster" + } + + return &ServiceInfo{ + Name: "redpanda", + Type: serviceType, + Status: "running", + Version: version, + Ports: ports, + Config: make(map[string]any), + ClusterNodes: clusterNodes, + } +} + +// detectSentry обнаруживает Sentry +func detectSentry() *ServiceInfo { + if !isProcessRunning("sentry") && !isProcessRunning("python.*sentry") { + return nil + } + + ports := getListeningPorts(9000, 9001) + version := "unknown" + + // Пробуем получить версию через HTTP API + versionOutput, err := runCommand("curl", "-s", "http://localhost:9000/api/0/") + if err == nil { + re := regexp.MustCompile(`"version"\s*:\s*"([^"]+)"`) + matches := re.FindStringSubmatch(versionOutput) + if len(matches) > 1 { + version = matches[1] + } + } + + return &ServiceInfo{ + Name: "sentry", + Type: "standalone", + Status: "running", + Version: version, + Ports: ports, + Config: make(map[string]any), + ClusterNodes: []string{"127.0.0.1"}, + } +} + +// detectSuperset обнаруживает Apache Superset +func detectSuperset() *ServiceInfo { + if !isProcessRunning("superset") && !isProcessRunning("python.*superset") { + return nil + } + + ports := getListeningPorts(8088) + version := "unknown" + + // Пробуем получить версию через HTTP API + versionOutput, err := runCommand("curl", "-s", "http://localhost:8088/api/v1/version") + if err == nil { + re := regexp.MustCompile(`"version"\s*:\s*"([^"]+)"`) + matches := re.FindStringSubmatch(versionOutput) + if len(matches) > 1 { + version = matches[1] + } + } + + return &ServiceInfo{ + Name: "superset", + Type: "standalone", + Status: "running", + Version: version, + Ports: ports, + Config: make(map[string]any), + ClusterNodes: []string{"127.0.0.1"}, + } +} + +// detectNats обнаруживает NATS +func detectNats() *ServiceInfo { + if !isProcessRunning("nats-server") { + return nil + } + + ports := getListeningPorts(4222, 8222) + version := "unknown" + + versionOutput, err := runCommand("nats-server", "--version") + if err == nil { + re := regexp.MustCompile(`(\d+\.\d+\.\d+)`) + matches := re.FindStringSubmatch(versionOutput) + if len(matches) > 1 { + version = matches[1] + } + } + + // Получаем ноды NATS кластера + clusterNodes := getNatsClusterNodes() + serviceType := "standalone" + if len(clusterNodes) > 1 { + serviceType = "cluster" + } + + return &ServiceInfo{ + Name: "nats", + Type: serviceType, + Status: "running", + Version: version, + Ports: ports, + Config: make(map[string]any), + ClusterNodes: clusterNodes, + } +} + +// detectInfluxDB обнаруживает InfluxDB +func detectInfluxDB() *ServiceInfo { + if !isProcessRunning("influxd") { + return nil + } + + ports := getListeningPorts(8086) + version := "unknown" + + versionOutput, err := runCommand("influxd", "version") + if err == nil { + re := regexp.MustCompile(`(\d+\.\d+\.\d+)`) + matches := re.FindStringSubmatch(versionOutput) + if len(matches) > 1 { + version = matches[1] + } + } + + return &ServiceInfo{ + Name: "influxdb", + Type: "standalone", + Status: "running", + Version: version, + Ports: ports, + Config: make(map[string]any), + ClusterNodes: []string{"127.0.0.1"}, + } +} + +// detectVictoriaMetrics обнаруживает VictoriaMetrics +func detectVictoriaMetrics() *ServiceInfo { + if !isProcessRunning("victoria-metrics") && !isProcessRunning("vmagent") { + return nil + } + + ports := getListeningPorts(8428, 8429) + version := "unknown" + + versionOutput, err := runCommand("victoria-metrics", "--version") + if err == nil { + re := regexp.MustCompile(`(\d+\.\d+\.\d+)`) + matches := re.FindStringSubmatch(versionOutput) + if len(matches) > 1 { + version = matches[1] + } + } + + return &ServiceInfo{ + Name: "victoriametrics", + Type: "standalone", + Status: "running", + Version: version, + Ports: ports, + Config: make(map[string]any), + ClusterNodes: []string{"127.0.0.1"}, + } +} + +// getDragonflyDBClusterNodes получает ноды DragonflyDB кластера +func getDragonflyDBClusterNodes() []string { + var nodes []string + + // Пробуем получить информацию о кластере + output, err := runCommand("dragonfly", "cluster", "nodes") + if err != nil { + // Если не удалось, возвращаем localhost + return []string{"127.0.0.1"} + } + + // Парсим вывод cluster nodes + lines := strings.Split(output, "\n") + for _, line := range lines { + line = strings.TrimSpace(line) + if line == "" { + continue + } + + // Парсим строку типа: "127.0.0.1:6379" + if strings.Contains(line, ":") { + ip := strings.Split(line, ":")[0] + if isValidIP(ip) { + nodes = append(nodes, ip) + } + } + } + + // Если не удалось получить ноды кластера, возвращаем localhost + if len(nodes) == 0 { + nodes = []string{"127.0.0.1"} + } + + return nodes +} + +// getElasticsearchClusterNodes получает ноды Elasticsearch кластера +func getElasticsearchClusterNodes() []string { + var nodes []string + + // Пробуем получить информацию о кластере через HTTP API + output, err := runCommand("curl", "-s", "http://localhost:9200/_cluster/state/nodes") + if err != nil { + // Если не удалось, возвращаем localhost + return []string{"127.0.0.1"} + } + + // Парсим JSON ответ (упрощенная версия) + re := regexp.MustCompile(`"([0-9.]+)"\s*:\s*\{`) + matches := re.FindAllStringSubmatch(output, -1) + for _, match := range matches { + if len(match) > 1 { + ip := match[1] + if isValidIP(ip) { + nodes = append(nodes, ip) + } + } + } + + // Если не удалось получить ноды кластера, возвращаем localhost + if len(nodes) == 0 { + nodes = []string{"127.0.0.1"} + } + + return nodes +} + +// getGreenplumClusterNodes получает ноды Greenplum кластера +func getGreenplumClusterNodes() []string { + var nodes []string + + // Пробуем получить информацию о кластере + output, err := runCommand("gpstate", "-s") + if err != nil { + // Если не удалось, возвращаем localhost + return []string{"127.0.0.1"} + } + + // Парсим вывод gpstate + lines := strings.Split(output, "\n") + for _, line := range lines { + line = strings.TrimSpace(line) + if strings.Contains(line, "Hostname") { + // Извлекаем hostname из строки типа: "Hostname: 10.14.246.75" + parts := strings.Split(line, ":") + if len(parts) > 1 { + hostname := strings.TrimSpace(parts[1]) + if ip := resolveHostname(hostname); ip != "" { + nodes = append(nodes, ip) + } else { + nodes = append(nodes, hostname) + } + } + } + } + + // Если не удалось получить ноды кластера, возвращаем localhost + if len(nodes) == 0 { + nodes = []string{"127.0.0.1"} + } + + return nodes +} + +// getMinioClusterNodes получает ноды MinIO кластера +func getMinioClusterNodes() []string { + var nodes []string + + // Пробуем получить информацию о кластере + output, err := runCommand("mc", "admin", "info", "local") + if err != nil { + // Если не удалось, возвращаем localhost + return []string{"127.0.0.1"} + } + + // Парсим вывод mc admin info + lines := strings.Split(output, "\n") + for _, line := range lines { + line = strings.TrimSpace(line) + if strings.Contains(line, "Endpoint") { + // Извлекаем endpoint из строки типа: "Endpoint: http://10.14.246.75:9000" + re := regexp.MustCompile(`http://([^:]+):`) + matches := re.FindStringSubmatch(line) + if len(matches) > 1 { + ip := matches[1] + if isValidIP(ip) { + nodes = append(nodes, ip) + } + } + } + } + + // Если не удалось получить ноды кластера, возвращаем localhost + if len(nodes) == 0 { + nodes = []string{"127.0.0.1"} + } + + return nodes +} + +// getRedpandaClusterNodes получает ноды Redpanda кластера +func getRedpandaClusterNodes() []string { + var nodes []string + + // Пробуем получить информацию о кластере + output, err := runCommand("rpk", "cluster", "info") + if err != nil { + // Если не удалось, возвращаем localhost + return []string{"127.0.0.1"} + } + + // Парсим вывод rpk cluster info + lines := strings.Split(output, "\n") + for _, line := range lines { + line = strings.TrimSpace(line) + if strings.Contains(line, "broker") { + // Извлекаем IP из строки типа: "broker-0: 10.14.246.75:9092" + re := regexp.MustCompile(`(\d+\.\d+\.\d+\.\d+):`) + matches := re.FindStringSubmatch(line) + if len(matches) > 1 { + ip := matches[1] + if isValidIP(ip) { + nodes = append(nodes, ip) + } + } + } + } + + // Если не удалось получить ноды кластера, возвращаем localhost + if len(nodes) == 0 { + nodes = []string{"127.0.0.1"} + } + + return nodes +} + +// getNatsClusterNodes получает ноды NATS кластера +func getNatsClusterNodes() []string { + var nodes []string + + // Пробуем получить информацию о кластере + output, err := runCommand("nats", "server", "list") + if err != nil { + // Если не удалось, возвращаем localhost + return []string{"127.0.0.1"} + } + + // Парсим вывод nats server list + lines := strings.Split(output, "\n") + for _, line := range lines { + line = strings.TrimSpace(line) + if strings.Contains(line, "nats://") { + // Извлекаем IP из строки типа: "nats://10.14.246.75:4222" + re := regexp.MustCompile(`nats://([^:]+):`) + matches := re.FindStringSubmatch(line) + if len(matches) > 1 { + ip := matches[1] + if isValidIP(ip) { + nodes = append(nodes, ip) + } + } + } + } + + // Если не удалось получить ноды кластера, возвращаем localhost + if len(nodes) == 0 { + nodes = []string{"127.0.0.1"} + } + + return nodes +}