diff --git a/src/collectors/proxvmservices/proxvmservices_linux.go b/src/collectors/proxvmservices/proxvmservices_linux.go index ece6f1c..797b4ed 100644 --- a/src/collectors/proxvmservices/proxvmservices_linux.go +++ b/src/collectors/proxvmservices/proxvmservices_linux.go @@ -19,14 +19,15 @@ import ( // ServiceInfo представляет информацию о сервисе type ServiceInfo struct { - Name string `json:"name"` - Type string `json:"type"` // standalone/cluster - Status string `json:"status"` // running/stopped/unknown - Version string `json:"version"` - Ports []int `json:"ports"` - Config map[string]any `json:"config"` - Cluster interface{} `json:"cluster,omitempty"` - Connections []ConnectionInfo `json:"connections,omitempty"` + Name string `json:"name"` + Type string `json:"type"` // standalone/cluster + Status string `json:"status"` // running/stopped/unknown + Version string `json:"version"` + Ports []int `json:"ports"` + Config map[string]any `json:"config"` + Cluster interface{} `json:"cluster,omitempty"` + ClusterNodes []string `json:"cluster_nodes,omitempty"` // IP всех нод кластера + Connections []ConnectionInfo `json:"connections,omitempty"` } // ConnectionInfo представляет информацию о соединениях @@ -206,15 +207,19 @@ func detectPostgreSQL() *ServiceInfo { // Получаем информацию о репликации connections := getPostgreSQLConnections() + // Извлекаем IP всех нод кластера + clusterNodes := extractClusterNodes(cluster) + return &ServiceInfo{ - Name: "postgresql", - Type: determineServiceType(cluster), - Status: "running", - Version: version, - Ports: ports, - Config: config, - Cluster: cluster, - Connections: connections, + Name: "postgresql", + Type: determineServiceType(cluster), + Status: "running", + Version: version, + Ports: ports, + Config: config, + Cluster: cluster, + ClusterNodes: clusterNodes, + Connections: connections, } } @@ -472,6 +477,331 @@ func determineServiceType(cluster interface{}) string { return "standalone" } +// extractClusterNodes извлекает IP всех нод кластера из различных типов кластеров +func extractClusterNodes(cluster interface{}) []string { + var nodes []string + + if cluster == nil { + return nodes + } + + // Для Patroni кластера + if patroniCluster, ok := cluster.(*PatroniClusterInfo); ok && patroniCluster != nil { + for _, member := range patroniCluster.Members { + if member.Host != "" { + nodes = append(nodes, member.Host) + } + } + return nodes + } + + // Для etcd кластера + if etcdCluster, ok := cluster.(*EtcdClusterInfo); ok && etcdCluster != nil { + for _, member := range etcdCluster.Members { + // Извлекаем IP из client_urls + for _, url := range member.ClientURLs { + if ip := extractIPFromURL(url); ip != "" { + nodes = append(nodes, ip) + } + } + } + return nodes + } + + // Для Kubernetes кластера - ноды уже получены в detectKubernetes + // и переданы через clusterNodes, поэтому здесь ничего не делаем + + return nodes +} + +// extractIPFromURL извлекает IP из URL (например, "http://10.14.246.77:2379" -> "10.14.246.77") +func extractIPFromURL(url string) string { + // Убираем протокол + if strings.Contains(url, "://") { + parts := strings.Split(url, "://") + if len(parts) > 1 { + url = parts[1] + } + } + + // Извлекаем IP:порт + if strings.Contains(url, ":") { + parts := strings.Split(url, ":") + if len(parts) > 0 { + ip := parts[0] + // Проверяем, что это валидный IP + if isValidIP(ip) { + return ip + } + } + } + + return "" +} + +// isValidIP проверяет, является ли строка валидным IP адресом +func isValidIP(ip string) bool { + parts := strings.Split(ip, ".") + if len(parts) != 4 { + return false + } + + for _, part := range parts { + if num, err := strconv.Atoi(part); err != nil || num < 0 || num > 255 { + return false + } + } + + return true +} + +// getKubernetesNodes получает IP всех нод Kubernetes кластера +func getKubernetesNodes() []string { + var nodes []string + + // Получаем список нод + output, err := runCommand("kubectl", "get", "nodes", "-o", "jsonpath={.items[*].status.addresses[?(@.type==\"InternalIP\")].address}") + if err != nil { + // Пробуем альтернативный способ + output, err = runCommand("kubectl", "get", "nodes", "-o", "wide", "--no-headers") + if err != nil { + return nodes + } + + // Парсим вывод kubectl get nodes -o wide + lines := strings.Split(output, "\n") + for _, line := range lines { + line = strings.TrimSpace(line) + if line == "" { + continue + } + + // Парсим строку типа: "kube-dbrain-node01 Ready control-plane,master 236d v1.28.2 10.14.246.75 10.14.246.75 Ubuntu 22.04.3 LTS 5.15.0-91-generic containerd://1.7.6" + parts := strings.Fields(line) + if len(parts) >= 6 { + // IP обычно в 6-м поле (индекс 5) + ip := parts[5] + if isValidIP(ip) { + nodes = append(nodes, ip) + } + } + } + return nodes + } + + // Парсим JSONPath вывод + if output != "" { + ips := strings.Fields(output) + for _, ip := range ips { + if isValidIP(ip) { + nodes = append(nodes, ip) + } + } + } + + return nodes +} + +// getRedisClusterNodes получает ноды Redis кластера +func getRedisClusterNodes() []string { + var nodes []string + + // Пробуем получить информацию о кластере + output, err := runCommand("redis-cli", "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 + } + + // Парсим строку типа: "abc123... 127.0.0.1:7000@17000 master - 0 1234567890 1 connected 0-5460" + parts := strings.Fields(line) + if len(parts) >= 2 { + // IP:порт находится во втором поле + hostPort := parts[1] + if strings.Contains(hostPort, ":") { + ip := strings.Split(hostPort, ":")[0] + if isValidIP(ip) { + nodes = append(nodes, ip) + } + } + } + } + + // Если не удалось получить ноды кластера, возвращаем localhost + if len(nodes) == 0 { + nodes = []string{"127.0.0.1"} + } + + return nodes +} + +// getClickHouseClusterNodes получает ноды ClickHouse кластера +func getClickHouseClusterNodes() []string { + var nodes []string + + // Пробуем получить информацию о кластере через system.clusters + output, err := runCommand("clickhouse-client", "--query", "SELECT host_name FROM system.clusters") + if err != nil { + // Если не удалось, возвращаем localhost + return []string{"127.0.0.1"} + } + + // Парсим вывод + lines := strings.Split(output, "\n") + for _, line := range lines { + line = strings.TrimSpace(line) + if line == "" { + continue + } + + // Проверяем, является ли это IP адресом + if isValidIP(line) { + nodes = append(nodes, line) + } + } + + // Если не удалось получить ноды кластера, возвращаем localhost + if len(nodes) == 0 { + nodes = []string{"127.0.0.1"} + } + + return nodes +} + +// getRabbitMQClusterNodes получает ноды RabbitMQ кластера +func getRabbitMQClusterNodes() []string { + var nodes []string + + // Пробуем получить информацию о кластере + output, err := runCommand("rabbitmqctl", "cluster_status") + if err != nil { + // Если не удалось, возвращаем localhost + return []string{"127.0.0.1"} + } + + // Парсим вывод cluster_status + lines := strings.Split(output, "\n") + for _, line := range lines { + line = strings.TrimSpace(line) + if strings.Contains(line, "running_nodes") { + // Извлекаем ноды из строки типа: "running_nodes,['rabbit@node1','rabbit@node2']" + re := regexp.MustCompile(`'rabbit@([^']+)'`) + matches := re.FindAllStringSubmatch(line, -1) + for _, match := range matches { + if len(match) > 1 { + hostname := match[1] + // Пробуем разрешить hostname в IP + if ip := resolveHostname(hostname); ip != "" { + nodes = append(nodes, ip) + } else { + nodes = append(nodes, hostname) + } + } + } + break + } + } + + // Если не удалось получить ноды кластера, возвращаем localhost + if len(nodes) == 0 { + nodes = []string{"127.0.0.1"} + } + + return nodes +} + +// resolveHostname пытается разрешить hostname в IP адрес +func resolveHostname(hostname string) string { + // Простая проверка, является ли это уже IP + if isValidIP(hostname) { + return hostname + } + + // Пробуем разрешить через nslookup или getent + output, err := runCommand("getent", "hosts", hostname) + if err != nil { + return "" + } + + // Парсим вывод getent hosts + parts := strings.Fields(output) + if len(parts) > 0 { + ip := parts[0] + if isValidIP(ip) { + return ip + } + } + + return "" +} + +// getKafkaClusterNodes получает ноды Kafka кластера +func getKafkaClusterNodes() []string { + var nodes []string + + // Пробуем получить информацию о кластере через kafka-topics + _, err := runCommand("kafka-topics", "--bootstrap-server", "localhost:9092", "--list") + if err != nil { + // Если не удалось, возвращаем localhost + return []string{"127.0.0.1"} + } + + // Для Kafka пока возвращаем localhost, так как получение нод кластера + // требует более сложной логики через JMX или конфигурационные файлы + // TODO: улучшить обнаружение нод Kafka кластера + nodes = []string{"127.0.0.1"} + + return nodes +} + +// getMongoDBClusterNodes получает ноды MongoDB кластера +func getMongoDBClusterNodes() []string { + var nodes []string + + // Пробуем получить информацию о кластере + output, err := runCommand("mongosh", "--quiet", "--eval", "rs.status().members.map(m => m.name)") + if err != nil { + // Если не удалось, возвращаем localhost + return []string{"127.0.0.1"} + } + + // Парсим вывод MongoDB + lines := strings.Split(output, "\n") + for _, line := range lines { + line = strings.TrimSpace(line) + if line == "" || line == "null" { + continue + } + + // Убираем кавычки и скобки + line = strings.Trim(line, "[]\"'") + if strings.Contains(line, ":") { + // Извлекаем hostname:port + hostname := strings.Split(line, ":")[0] + 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 +} + // detectEtcd обнаруживает etcd func detectEtcd() *ServiceInfo { // Проверяем процессы @@ -491,14 +821,18 @@ func detectEtcd() *ServiceInfo { // Проверяем кластер cluster := getEtcdCluster() + // Извлекаем IP всех нод кластера + clusterNodes := extractClusterNodes(cluster) + return &ServiceInfo{ - Name: "etcd", - Type: "cluster", - Status: "running", - Version: version, - Ports: ports, - Config: config, - Cluster: cluster, + Name: "etcd", + Type: "cluster", + Status: "running", + Version: version, + Ports: ports, + Config: config, + Cluster: cluster, + ClusterNodes: clusterNodes, } } @@ -643,7 +977,7 @@ func parseEtcdMembers(output string) []EtcdMember { return members } -// Заглушки для остальных сервисов (пока не реализованы) +// detectRedis обнаруживает Redis (standalone или cluster) func detectRedis() *ServiceInfo { if !isProcessRunning("redis-server") { return nil @@ -661,13 +995,21 @@ func detectRedis() *ServiceInfo { } } + // Проверяем, является ли это Redis кластером + clusterNodes := getRedisClusterNodes() + serviceType := "standalone" + if len(clusterNodes) > 1 { + serviceType = "cluster" + } + return &ServiceInfo{ - Name: "redis", - Type: "standalone", - Status: "running", - Version: version, - Ports: ports, - Config: make(map[string]any), + Name: "redis", + Type: serviceType, + Status: "running", + Version: version, + Ports: ports, + Config: make(map[string]any), + ClusterNodes: clusterNodes, } } @@ -688,13 +1030,21 @@ func detectClickHouse() *ServiceInfo { } } + // Получаем ноды ClickHouse кластера + clusterNodes := getClickHouseClusterNodes() + serviceType := "standalone" + if len(clusterNodes) > 1 { + serviceType = "cluster" + } + return &ServiceInfo{ - Name: "clickhouse", - Type: "standalone", - Status: "running", - Version: version, - Ports: ports, - Config: make(map[string]any), + Name: "clickhouse", + Type: serviceType, + Status: "running", + Version: version, + Ports: ports, + Config: make(map[string]any), + ClusterNodes: clusterNodes, } } @@ -715,13 +1065,21 @@ func detectRabbitMQ() *ServiceInfo { } } + // Получаем ноды RabbitMQ кластера + clusterNodes := getRabbitMQClusterNodes() + serviceType := "standalone" + if len(clusterNodes) > 1 { + serviceType = "cluster" + } + return &ServiceInfo{ - Name: "rabbitmq", - Type: "standalone", - Status: "running", - Version: version, - Ports: ports, - Config: make(map[string]any), + Name: "rabbitmq", + Type: serviceType, + Status: "running", + Version: version, + Ports: ports, + Config: make(map[string]any), + ClusterNodes: clusterNodes, } } @@ -733,13 +1091,21 @@ func detectKafka() *ServiceInfo { ports := getListeningPorts(9092) version := "unknown" + // Получаем ноды Kafka кластера + clusterNodes := getKafkaClusterNodes() + serviceType := "standalone" + if len(clusterNodes) > 1 { + serviceType = "cluster" + } + return &ServiceInfo{ - Name: "kafka", - Type: "standalone", - Status: "running", - Version: version, - Ports: ports, - Config: make(map[string]any), + Name: "kafka", + Type: serviceType, + Status: "running", + Version: version, + Ports: ports, + Config: make(map[string]any), + ClusterNodes: clusterNodes, } } @@ -760,13 +1126,21 @@ func detectMongoDB() *ServiceInfo { } } + // Получаем ноды MongoDB кластера + clusterNodes := getMongoDBClusterNodes() + serviceType := "standalone" + if len(clusterNodes) > 1 { + serviceType = "cluster" + } + return &ServiceInfo{ - Name: "mongodb", - Type: "standalone", - Status: "running", - Version: version, - Ports: ports, - Config: make(map[string]any), + Name: "mongodb", + Type: serviceType, + Status: "running", + Version: version, + Ports: ports, + Config: make(map[string]any), + ClusterNodes: clusterNodes, } } @@ -787,12 +1161,16 @@ func detectKubernetes() *ServiceInfo { } } + // Получаем информацию о нодах кластера + clusterNodes := getKubernetesNodes() + return &ServiceInfo{ - Name: "kubernetes", - Type: "cluster", - Status: "running", - Version: version, - Ports: ports, - Config: make(map[string]any), + Name: "kubernetes", + Type: "cluster", + Status: "running", + Version: version, + Ports: ports, + Config: make(map[string]any), + ClusterNodes: clusterNodes, } }