diff --git a/agent/agent.yml.sample b/agent/agent.yml.sample new file mode 100644 index 0000000..76f4a32 --- /dev/null +++ b/agent/agent.yml.sample @@ -0,0 +1,14 @@ +# Configuración del agente de monitoreo u-site +# Copia este archivo como agent.yml y edítalo. + +# URL del panel de administración (sin barra al final) +api_url: https://admin.u-site.app + +# Token generado desde el panel en Servidor → Configurar Agente +token: PEGAR_TOKEN_AQUI + +# Intervalo de reporte en segundos (mínimo recomendado: 30) +interval: 30 + +# Activar logs detallados (true para debug) +debug: false diff --git a/agent/go.mod b/agent/go.mod new file mode 100644 index 0000000..a8ca67c --- /dev/null +++ b/agent/go.mod @@ -0,0 +1,19 @@ +module usite-agent + +go 1.21 + +require ( + github.com/shirou/gopsutil/v3 v3.24.4 + gopkg.in/yaml.v3 v3.0.1 +) + +require ( + github.com/go-ole/go-ole v1.3.0 // indirect + github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect + github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect + github.com/shoenig/go-m1cpu v0.1.6 // indirect + github.com/tklauser/go-sysconf v0.3.12 // indirect + github.com/tklauser/numcpus v0.6.1 // indirect + github.com/yusufpapurcu/wmi v1.2.4 // indirect + golang.org/x/sys v0.20.0 // indirect +) diff --git a/agent/go.sum b/agent/go.sum new file mode 100644 index 0000000..3126c21 --- /dev/null +++ b/agent/go.sum @@ -0,0 +1,51 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= +github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/shirou/gopsutil/v3 v3.24.4 h1:dEHgzZXt4LMNm+oYELpzl9YCqV65Yr/6SfrvgRBtXeU= +github.com/shirou/gopsutil/v3 v3.24.4/go.mod h1:lTd2mdiOspcqLgAnr9/nGi71NkeMpWKdmhuxm9GusH8= +github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= +github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= +github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU= +github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= +github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= +github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= +github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= +github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= +github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/agent/install.sh b/agent/install.sh new file mode 100644 index 0000000..5d08cda --- /dev/null +++ b/agent/install.sh @@ -0,0 +1,114 @@ +#!/usr/bin/env bash +# install.sh — Instalador del agente usite-agent +# Uso: +# curl -sL https://admin.u-site.app/agent/install.sh | bash -s -- --token=TU_TOKEN --url=https://admin.u-site.app +# o descargando primero: +# bash install.sh --token=TU_TOKEN --url=https://admin.u-site.app [--interval=30] [--dir=/opt/usite-agent] + +set -euo pipefail + +# ── Colores ─────────────────────────────────────────────────────────────────── +RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; BLUE='\033[0;34m'; NC='\033[0m' +info() { echo -e "${BLUE}[INFO]${NC} $*"; } +ok() { echo -e "${GREEN}[OK]${NC} $*"; } +warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } +error() { echo -e "${RED}[ERROR]${NC} $*"; exit 1; } + +# ── Parámetros ──────────────────────────────────────────────────────────────── +TOKEN="" +API_URL="" +INTERVAL=30 +INSTALL_DIR="/opt/usite-agent" + +for arg in "$@"; do + case $arg in + --token=*) TOKEN="${arg#*=}" ;; + --url=*) API_URL="${arg#*=}" ;; + --interval=*) INTERVAL="${arg#*=}" ;; + --dir=*) INSTALL_DIR="${arg#*=}" ;; + esac +done + +[[ -z "$TOKEN" ]] && error "Falta --token=TU_TOKEN" +[[ -z "$API_URL" ]] && error "Falta --url=https://admin.u-site.app" + +# ── Detectar arquitectura ───────────────────────────────────────────────────── +OS="$(uname -s | tr '[:upper:]' '[:lower:]')" +ARCH="$(uname -m)" +case $ARCH in + x86_64) ARCH="amd64" ;; + aarch64) ARCH="arm64" ;; + armv7l) ARCH="arm" ;; + *) error "Arquitectura no soportada: $ARCH" ;; +esac + +BINARY_NAME="usite-agent-${OS}-${ARCH}" +DOWNLOAD_URL="${API_URL}/agent/download/${BINARY_NAME}" + +info "Sistema: ${OS}/${ARCH}" +info "Directorio de instalación: ${INSTALL_DIR}" +info "Intervalo de reporte: ${INTERVAL}s" + +# ── Crear directorio ────────────────────────────────────────────────────────── +mkdir -p "$INSTALL_DIR" + +# ── Descargar binario ───────────────────────────────────────────────────────── +info "Descargando agente desde ${DOWNLOAD_URL}..." +if command -v curl &>/dev/null; then + curl -fsSL "$DOWNLOAD_URL" -o "${INSTALL_DIR}/usite-agent" || error "No se pudo descargar el binario" +elif command -v wget &>/dev/null; then + wget -q "$DOWNLOAD_URL" -O "${INSTALL_DIR}/usite-agent" || error "No se pudo descargar el binario" +else + error "Se requiere curl o wget para descargar el agente" +fi +chmod +x "${INSTALL_DIR}/usite-agent" +ok "Binario descargado y listo" + +# ── Crear configuración ─────────────────────────────────────────────────────── +cat > "${INSTALL_DIR}/agent.yml" </dev/null; then + cat > /etc/systemd/system/usite-agent.service < 0 { + return fmt.Sprintf("%dd %dh %dm", d, h, m) + } + return fmt.Sprintf("%dh %dm", h, m) +} + +func collectMetrics() (*Metricas, error) { + m := &Metricas{ + OS: runtime.GOOS, + Arch: runtime.GOARCH, + ReportadoEn: time.Now().UTC().Format(time.RFC3339), + } + + // Hostname + if hn, err := os.Hostname(); err == nil { + m.Hostname = hn + } + + // Uptime + if info, err := host.Info(); err == nil { + m.Uptime = info.Uptime + m.UptimeStr = uptimeStr(info.Uptime) + if info.OS != "" { + m.OS = info.Platform + " " + info.PlatformVersion + } + } + + // RAM + if v, err := mem.VirtualMemory(); err == nil { + gb := 1024.0 * 1024 * 1024 + m.RAM = RAMInfo{ + TotalGB: round2(float64(v.Total) / gb), + UsadoGB: round2(float64(v.Used) / gb), + LibreGB: round2(float64(v.Free) / gb), + Porcentaje: round2(v.UsedPercent), + } + } + + // CPU + m.CPU.Nucleos = runtime.NumCPU() + if pcts, err := cpu.Percent(1*time.Second, false); err == nil && len(pcts) > 0 { + m.CPU.Porcentaje = round2(pcts[0]) + } + + // Disco (raíz /) + root := "/" + if runtime.GOOS == "windows" { + root = "C:\\" + } + if d, err := disk.Usage(root); err == nil { + gb := 1024.0 * 1024 * 1024 + m.Disco = DiscoInfo{ + TotalGB: round2(float64(d.Total) / gb), + UsadoGB: round2(float64(d.Used) / gb), + LibreGB: round2(float64(d.Free) / gb), + Porcentaje: round2(d.UsedPercent), + Ruta: root, + } + } + + // Load average (Linux/macOS) + if la, err := load.Avg(); err == nil { + m.LoadAvg = LoadInfo{Load1: round2(la.Load1), Load5: round2(la.Load5), Load15: round2(la.Load15)} + } + + return m, nil +} + +// ── Reporte ─────────────────────────────────────────────────────────────────── + +type HeartbeatRequest struct { + Token string `json:"token"` + Metricas string `json:"metricas"` +} + +func sendHeartbeat(cfg *Config, metricas *Metricas) error { + metJSON, err := json.Marshal(metricas) + if err != nil { + return err + } + + payload := HeartbeatRequest{ + Token: cfg.Token, + Metricas: string(metJSON), + } + body, _ := json.Marshal(payload) + + url := cfg.APIURL + "/agent/heartbeat" + req, err := http.NewRequest("POST", url, bytes.NewReader(body)) + if err != nil { + return err + } + req.Header.Set("Content-Type", "application/json") + req.Header.Set("User-Agent", "usite-agent/1.0") + + client := &http.Client{Timeout: 10 * time.Second} + resp, err := client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("API respondió %d", resp.StatusCode) + } + return nil +} + +// ── Main ────────────────────────────────────────────────────────────────────── + +func main() { + configPath := flag.String("config", "agent.yml", "ruta al archivo de configuración") + apiURL := flag.String("api-url", "", "URL del API (sobreescribe config)") + token := flag.String("token", "", "token del servidor (sobreescribe config)") + flag.Parse() + + cfg, err := loadConfig(*configPath) + if err != nil { + // Si no hay config pero se pasaron flags, usarlos directamente + if *apiURL == "" || *token == "" { + log.Fatalf("Error cargando config: %v\nUso: usite-agent --api-url=https://... --token=...", err) + } + cfg = &Config{APIURL: *apiURL, Token: *token, Interval: 30} + } + + // Flags sobreescriben config + if *apiURL != "" { + cfg.APIURL = *apiURL + } + if *token != "" { + cfg.Token = *token + } + + if cfg.APIURL == "" || cfg.Token == "" { + log.Fatal("api_url y token son requeridos") + } + + log.Printf("🚀 usite-agent iniciado | API: %s | Intervalo: %ds", cfg.APIURL, cfg.Interval) + + ticker := time.NewTicker(time.Duration(cfg.Interval) * time.Second) + defer ticker.Stop() + + // Enviar inmediatamente al iniciar + reportar := func() { + m, err := collectMetrics() + if err != nil { + log.Printf("⚠️ Error recolectando métricas: %v", err) + return + } + if cfg.Debug { + b, _ := json.MarshalIndent(m, "", " ") + log.Printf("📊 Métricas:\n%s", string(b)) + } + if err := sendHeartbeat(cfg, m); err != nil { + log.Printf("⚠️ Error enviando heartbeat: %v", err) + } else { + log.Printf("✅ Heartbeat enviado | RAM: %.1f%% | CPU: %.1f%% | Disco: %.1f%%", + m.RAM.Porcentaje, m.CPU.Porcentaje, m.Disco.Porcentaje) + } + } + + reportar() + for range ticker.C { + reportar() + } +} diff --git a/pkg/models/servidor.go b/pkg/models/servidor.go index a426b94..4ed5a73 100755 --- a/pkg/models/servidor.go +++ b/pkg/models/servidor.go @@ -2,6 +2,7 @@ package models import ( "log" + "time" "github.com/sujit-baniya/fiber-boilerplate/app" "gorm.io/gorm" @@ -21,6 +22,10 @@ type Servidor struct { ProvServidor ProvServidor `json:"prov_servidor" gorm:"foreignKey:ProvServidorID"` TipoServidorID uint `json:"tipo_servidor_id" gorm:"column:tipo_servidor_id;foreignKey:TipoServidorID"` TipoServidor TipoServidor `json:"tipo_servidor" gorm:"foreignKey:TipoServidorID"` + // Campos del agente de monitoreo + AgentToken string `json:"agent_token" gorm:"column:agent_token;uniqueIndex"` + AgentLastSeen *time.Time `json:"agent_last_seen" gorm:"column:agent_last_seen"` + MetricasJson string `json:"metricas_json" gorm:"column:metricas_json;type:text"` } func (Servidor) TableName() string { diff --git a/resources/views/servidor_dashboard.html b/resources/views/servidor_dashboard.html index be01aa6..de521c3 100644 --- a/resources/views/servidor_dashboard.html +++ b/resources/views/servidor_dashboard.html @@ -36,61 +36,124 @@

- +
+ + + + ● En línea + ● Fuera + ○ Sin agente + +
- -
-
-

RAM

-

-
-
-

CPU

-

-
-
-

Disco

-

-
-
- -
+ + + + + + + +
- -
- - -
- Último ping: -
- - -
- - - - + - conexiones BD + BD
- - + +
+ + +
@@ -274,11 +337,145 @@ + +
+
+ + +
+
+

⚡ Agente de Monitoreo

+

+
+ +
+ +
+ + +
+
+
+
+

+ ✅ Agente en línea + ⚠️ Agente instalado pero sin reportes recientes + 🔧 Agente no configurado +

+

+

+
+
+ + + +
+ + +
+
+

🔑 Token del servidor

+ +
+ + +
+ + + + + + + +
+
+
+