onboarding: auto-create default checks when entity is created
Three insertion points: - CreateEntity (POST /api/v1/entities) - EnrollClient (POST /api/v1/clients/enroll) - seed.go (seed ingest at deploy time) Shared logic in internal/checkdefaults — resolves host IP from lan_ip > mesh.netbird.ip > mesh_ip, SSH user/port from attributes. Default checks per entity type: - proxmox-host/standalone-server: ping + cpu + memory + load + disk + updates - workstation: ping + cpu + memory + load - lxc: cpu + memory + load + disk - vm: ping - service: process_check.sh All idempotent (ON CONFLICT DO NOTHING). New machines now get monitoring automatically — no manual curl calls needed.
This commit is contained in:
204
internal/checkdefaults/defaults.go
Normal file
204
internal/checkdefaults/defaults.go
Normal file
@@ -0,0 +1,204 @@
|
|||||||
|
package checkdefaults
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/jackc/pgx/v5"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CheckDef struct {
|
||||||
|
Kind string
|
||||||
|
Script string
|
||||||
|
Host string
|
||||||
|
User string
|
||||||
|
Port int
|
||||||
|
Thresholds map[string]any
|
||||||
|
Extra map[string]any
|
||||||
|
}
|
||||||
|
|
||||||
|
func ResolveHost(attrs map[string]any) string {
|
||||||
|
if ip, ok := attrs["lan_ip"].(string); ok && ip != "" {
|
||||||
|
return ip
|
||||||
|
}
|
||||||
|
if mesh, ok := attrs["mesh"].(map[string]any); ok {
|
||||||
|
if nb, ok := mesh["netbird"].(map[string]any); ok {
|
||||||
|
if ip, ok := nb["ip"].(string); ok && ip != "" {
|
||||||
|
return ip
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ip, ok := attrs["mesh_ip"].(string); ok && ip != "" {
|
||||||
|
return ip
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func resolveSSHUser(attrs map[string]any) string {
|
||||||
|
if ssh, ok := attrs["ssh"].(map[string]any); ok {
|
||||||
|
if u, ok := ssh["user"].(string); ok && u != "" {
|
||||||
|
return u
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "root"
|
||||||
|
}
|
||||||
|
|
||||||
|
func resolveSSHPort(attrs map[string]any) int {
|
||||||
|
if ssh, ok := attrs["ssh"].(map[string]any); ok {
|
||||||
|
switch p := ssh["port"].(type) {
|
||||||
|
case float64:
|
||||||
|
return int(p)
|
||||||
|
case int:
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 22
|
||||||
|
}
|
||||||
|
|
||||||
|
func ForEntityType(entityType string, attrs map[string]any) []CheckDef {
|
||||||
|
host := ResolveHost(attrs)
|
||||||
|
user := resolveSSHUser(attrs)
|
||||||
|
port := resolveSSHPort(attrs)
|
||||||
|
|
||||||
|
ssh := func(script string) CheckDef {
|
||||||
|
return CheckDef{Kind: "ssh-script", Script: script, Host: host, User: user, Port: port}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch entityType {
|
||||||
|
case "proxmox-host", "standalone-server":
|
||||||
|
if host == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return []CheckDef{
|
||||||
|
{Kind: "ping", Host: host},
|
||||||
|
ssh("cpu_check.sh"),
|
||||||
|
ssh("memory_check.sh"),
|
||||||
|
ssh("load_check.sh"),
|
||||||
|
ssh("disk_usage_check.sh"),
|
||||||
|
ssh("updates_check.sh"),
|
||||||
|
}
|
||||||
|
case "workstation":
|
||||||
|
if host == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return []CheckDef{
|
||||||
|
{Kind: "ping", Host: host},
|
||||||
|
ssh("cpu_check.sh"),
|
||||||
|
ssh("memory_check.sh"),
|
||||||
|
ssh("load_check.sh"),
|
||||||
|
}
|
||||||
|
case "lxc":
|
||||||
|
if host == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return []CheckDef{
|
||||||
|
ssh("cpu_check.sh"),
|
||||||
|
ssh("memory_check.sh"),
|
||||||
|
ssh("load_check.sh"),
|
||||||
|
ssh("disk_usage_check.sh"),
|
||||||
|
}
|
||||||
|
case "vm":
|
||||||
|
if host == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return []CheckDef{
|
||||||
|
{Kind: "ping", Host: host},
|
||||||
|
}
|
||||||
|
case "service":
|
||||||
|
if host == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
n, _ := attrs["name"].(string)
|
||||||
|
if n == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return []CheckDef{
|
||||||
|
{Kind: "ssh-script", Script: "process_check.sh", Host: host, User: user, Port: port,
|
||||||
|
Extra: map[string]any{"args": n}},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ShortSlug(slug string) string {
|
||||||
|
const n = 8
|
||||||
|
if len(slug) > n {
|
||||||
|
return slug[len(slug)-n:]
|
||||||
|
}
|
||||||
|
return slug
|
||||||
|
}
|
||||||
|
|
||||||
|
func DefaultInterval(kind string) int32 {
|
||||||
|
switch kind {
|
||||||
|
case "ping":
|
||||||
|
return 30
|
||||||
|
case "ssh-script":
|
||||||
|
return 60
|
||||||
|
default:
|
||||||
|
return 300
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Ensure(ctx context.Context, tx pgx.Tx, entityID uuid.UUID, slug, entityType string, attrsJSON []byte) {
|
||||||
|
_, _ = tx.Exec(ctx,
|
||||||
|
`INSERT INTO entity_status (entity_id, health, updated_at)
|
||||||
|
VALUES ($1, 'unknown', now())
|
||||||
|
ON CONFLICT (entity_id) DO NOTHING`,
|
||||||
|
entityID)
|
||||||
|
|
||||||
|
var attrs map[string]any
|
||||||
|
if len(attrsJSON) > 0 {
|
||||||
|
json.Unmarshal(attrsJSON, &attrs)
|
||||||
|
}
|
||||||
|
if attrs == nil {
|
||||||
|
attrs = map[string]any{}
|
||||||
|
}
|
||||||
|
|
||||||
|
defs := ForEntityType(entityType, attrs)
|
||||||
|
if len(defs) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, def := range defs {
|
||||||
|
checkID, err := uuid.NewV7()
|
||||||
|
if err != nil {
|
||||||
|
checkID = uuid.New()
|
||||||
|
}
|
||||||
|
checkSlug := fmt.Sprintf("check:%s:%s:%d", def.Kind, ShortSlug(slug), i)
|
||||||
|
|
||||||
|
_, _ = tx.Exec(ctx,
|
||||||
|
`INSERT INTO entities (id, slug, type, name, state, attributes, version, created_at, updated_at)
|
||||||
|
VALUES ($1, $2, 'check', $2, 'active', '{}', 1, now(), now())
|
||||||
|
ON CONFLICT (slug) DO NOTHING`,
|
||||||
|
checkID, checkSlug)
|
||||||
|
|
||||||
|
configMap := map[string]any{}
|
||||||
|
if def.Script != "" {
|
||||||
|
configMap["script"] = def.Script
|
||||||
|
}
|
||||||
|
if def.Host != "" {
|
||||||
|
configMap["host"] = def.Host
|
||||||
|
}
|
||||||
|
if def.User != "" && def.User != "root" {
|
||||||
|
configMap["user"] = def.User
|
||||||
|
}
|
||||||
|
if def.Port != 0 && def.Port != 22 {
|
||||||
|
configMap["port"] = def.Port
|
||||||
|
}
|
||||||
|
if def.Thresholds != nil {
|
||||||
|
configMap["thresholds"] = def.Thresholds
|
||||||
|
}
|
||||||
|
for k, v := range def.Extra {
|
||||||
|
configMap[k] = v
|
||||||
|
}
|
||||||
|
configJSON, _ := json.Marshal(configMap)
|
||||||
|
|
||||||
|
_, _ = tx.Exec(ctx,
|
||||||
|
`INSERT INTO check_defs (entity_id, kind, config, interval_s, timeout_s, enabled)
|
||||||
|
VALUES ($1, $2, $3, $4, 30, true)
|
||||||
|
ON CONFLICT (entity_id) DO NOTHING`,
|
||||||
|
checkID, def.Kind, configJSON, DefaultInterval(def.Kind))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/dtoro/oikos/internal/checkdefaults"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/jackc/pgx/v5"
|
"github.com/jackc/pgx/v5"
|
||||||
)
|
)
|
||||||
@@ -143,6 +144,8 @@ func IngestInventorySeed(ctx context.Context, tx pgx.Tx, data map[string]any) (*
|
|||||||
return nil, fmt.Errorf("entity_status %s: %w", slug, err)
|
return nil, fmt.Errorf("entity_status %s: %w", slug, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
checkdefaults.Ensure(ctx, tx, entityID, slug, typeName, attrsBytes)
|
||||||
|
|
||||||
r.Entities++
|
r.Entities++
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
13
internal/httpapi/default_checks.go
Normal file
13
internal/httpapi/default_checks.go
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
package httpapi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/dtoro/oikos/internal/checkdefaults"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/jackc/pgx/v5"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ensureDefaultChecks(ctx context.Context, tx pgx.Tx, entityID uuid.UUID, slug, entityType string, attrsJSON []byte) {
|
||||||
|
checkdefaults.Ensure(ctx, tx, entityID, slug, entityType, attrsJSON)
|
||||||
|
}
|
||||||
@@ -955,6 +955,8 @@ func (s *Server) CreateEntity(ctx context.Context, req gen.CreateEntityRequestOb
|
|||||||
return nil, eventErr
|
return nil, eventErr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ensureDefaultChecks(ctx, tx, inserted.ID, slug, req.Body.Type, attrsJSON)
|
||||||
|
|
||||||
if err := tx.Commit(ctx); err != nil {
|
if err := tx.Commit(ctx); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -1219,6 +1221,8 @@ func (s *Server) EnrollClient(ctx context.Context, req gen.EnrollClientRequestOb
|
|||||||
"info", "oikos-api", "",
|
"info", "oikos-api", "",
|
||||||
map[string]any{"slug": req.Body.Slug, "type": current.Type})
|
map[string]any{"slug": req.Body.Slug, "type": current.Type})
|
||||||
|
|
||||||
|
ensureDefaultChecks(ctx, tx, id, req.Body.Slug, current.Type, attrsJSON)
|
||||||
|
|
||||||
if err := tx.Commit(ctx); err != nil {
|
if err := tx.Commit(ctx); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user