feat: добавлено обнаружение 18 новых сервисов в коллектор proxvmservices

Добавлены следующие сервисы с поддержкой кластеров:

**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
This commit is contained in:
Sergey Antropoff 2025-09-15 17:28:07 +03:00
parent ceab977da1
commit 7fbbb6d0f7

View File

@ -117,6 +117,62 @@ func collectProxVMServices(ctx context.Context) (map[string]any, error) {
if k8s := detectKubernetes(); k8s != nil { if k8s := detectKubernetes(); k8s != nil {
services = append(services, *k8s) 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 result["services"] = services
@ -1174,3 +1230,790 @@ func detectKubernetes() *ServiceInfo {
ClusterNodes: clusterNodes, 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
}