375 lines
14 KiB
Go
375 lines
14 KiB
Go
package router
|
|
|
|
import (
|
|
"context"
|
|
"time"
|
|
|
|
"github.com/atlasos/calypso/internal/audit"
|
|
"github.com/atlasos/calypso/internal/auth"
|
|
"github.com/atlasos/calypso/internal/backup"
|
|
"github.com/atlasos/calypso/internal/common/cache"
|
|
"github.com/atlasos/calypso/internal/common/config"
|
|
"github.com/atlasos/calypso/internal/common/database"
|
|
"github.com/atlasos/calypso/internal/common/logger"
|
|
"github.com/atlasos/calypso/internal/iam"
|
|
"github.com/atlasos/calypso/internal/monitoring"
|
|
"github.com/atlasos/calypso/internal/scst"
|
|
"github.com/atlasos/calypso/internal/storage"
|
|
"github.com/atlasos/calypso/internal/system"
|
|
"github.com/atlasos/calypso/internal/tape_physical"
|
|
"github.com/atlasos/calypso/internal/tape_vtl"
|
|
"github.com/atlasos/calypso/internal/tasks"
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
// NewRouter creates and configures the HTTP router
|
|
func NewRouter(cfg *config.Config, db *database.DB, log *logger.Logger) *gin.Engine {
|
|
if cfg.Logging.Level == "debug" {
|
|
gin.SetMode(gin.DebugMode)
|
|
} else {
|
|
gin.SetMode(gin.ReleaseMode)
|
|
}
|
|
|
|
r := gin.New()
|
|
|
|
// Initialize cache if enabled
|
|
var responseCache *cache.Cache
|
|
if cfg.Server.Cache.Enabled {
|
|
responseCache = cache.NewCache(cfg.Server.Cache.DefaultTTL)
|
|
log.Info("Response caching enabled", "default_ttl", cfg.Server.Cache.DefaultTTL)
|
|
}
|
|
|
|
// Middleware
|
|
r.Use(ginLogger(log))
|
|
r.Use(gin.Recovery())
|
|
r.Use(securityHeadersMiddleware(cfg))
|
|
r.Use(rateLimitMiddleware(cfg, log))
|
|
r.Use(corsMiddleware(cfg))
|
|
|
|
// Cache control headers (always applied)
|
|
r.Use(cacheControlMiddleware())
|
|
|
|
// Response caching middleware (if enabled)
|
|
if cfg.Server.Cache.Enabled {
|
|
cacheConfig := CacheConfig{
|
|
Enabled: cfg.Server.Cache.Enabled,
|
|
DefaultTTL: cfg.Server.Cache.DefaultTTL,
|
|
MaxAge: cfg.Server.Cache.MaxAge,
|
|
}
|
|
r.Use(cacheMiddleware(cacheConfig, responseCache))
|
|
}
|
|
|
|
// Initialize monitoring services
|
|
eventHub := monitoring.NewEventHub(log)
|
|
alertService := monitoring.NewAlertService(db, log)
|
|
alertService.SetEventHub(eventHub) // Connect alert service to event hub
|
|
metricsService := monitoring.NewMetricsService(db, log)
|
|
healthService := monitoring.NewHealthService(db, log, metricsService)
|
|
|
|
// Start event hub in background
|
|
go eventHub.Run()
|
|
|
|
// Start metrics broadcaster in background
|
|
go func() {
|
|
ticker := time.NewTicker(30 * time.Second) // Broadcast metrics every 30 seconds
|
|
defer ticker.Stop()
|
|
for {
|
|
select {
|
|
case <-ticker.C:
|
|
if metrics, err := metricsService.CollectMetrics(context.Background()); err == nil {
|
|
eventHub.BroadcastMetrics(metrics)
|
|
}
|
|
}
|
|
}
|
|
}()
|
|
|
|
// Initialize and start alert rule engine
|
|
alertRuleEngine := monitoring.NewAlertRuleEngine(db, log, alertService)
|
|
|
|
// Register default alert rules
|
|
alertRuleEngine.RegisterRule(monitoring.NewAlertRule(
|
|
"storage-capacity-warning",
|
|
"Storage Capacity Warning",
|
|
monitoring.AlertSourceStorage,
|
|
&monitoring.StorageCapacityCondition{ThresholdPercent: 80.0},
|
|
monitoring.AlertSeverityWarning,
|
|
true,
|
|
"Alert when storage repositories exceed 80% capacity",
|
|
))
|
|
alertRuleEngine.RegisterRule(monitoring.NewAlertRule(
|
|
"storage-capacity-critical",
|
|
"Storage Capacity Critical",
|
|
monitoring.AlertSourceStorage,
|
|
&monitoring.StorageCapacityCondition{ThresholdPercent: 95.0},
|
|
monitoring.AlertSeverityCritical,
|
|
true,
|
|
"Alert when storage repositories exceed 95% capacity",
|
|
))
|
|
alertRuleEngine.RegisterRule(monitoring.NewAlertRule(
|
|
"task-failure",
|
|
"Task Failure",
|
|
monitoring.AlertSourceTask,
|
|
&monitoring.TaskFailureCondition{LookbackMinutes: 60},
|
|
monitoring.AlertSeverityWarning,
|
|
true,
|
|
"Alert when tasks fail within the last hour",
|
|
))
|
|
|
|
// Start alert rule engine in background
|
|
ctx := context.Background()
|
|
go alertRuleEngine.Start(ctx)
|
|
|
|
// Health check (no auth required) - enhanced
|
|
r.GET("/api/v1/health", func(c *gin.Context) {
|
|
health := healthService.CheckHealth(c.Request.Context())
|
|
statusCode := 200
|
|
if health.Status == "unhealthy" {
|
|
statusCode = 503
|
|
} else if health.Status == "degraded" {
|
|
statusCode = 200 // Still 200 but with degraded status
|
|
}
|
|
c.JSON(statusCode, health)
|
|
})
|
|
|
|
// API v1 routes
|
|
v1 := r.Group("/api/v1")
|
|
{
|
|
// Auth routes (public)
|
|
authHandler := auth.NewHandler(db, cfg, log)
|
|
v1.POST("/auth/login", authHandler.Login)
|
|
v1.POST("/auth/logout", authHandler.Logout)
|
|
|
|
// Audit middleware for mutating operations (applied to all v1 routes)
|
|
auditMiddleware := audit.NewMiddleware(db, log)
|
|
v1.Use(auditMiddleware.LogRequest())
|
|
|
|
// Protected routes
|
|
protected := v1.Group("")
|
|
protected.Use(authMiddleware(authHandler))
|
|
protected.Use(func(c *gin.Context) {
|
|
// Store DB in context for permission middleware
|
|
c.Set("db", db)
|
|
c.Next()
|
|
})
|
|
{
|
|
// Auth
|
|
protected.GET("/auth/me", authHandler.Me)
|
|
|
|
// Tasks
|
|
taskHandler := tasks.NewHandler(db, log)
|
|
protected.GET("/tasks/:id", taskHandler.GetTask)
|
|
|
|
// Storage
|
|
storageHandler := storage.NewHandler(db, log)
|
|
// Pass cache to storage handler for cache invalidation
|
|
if responseCache != nil {
|
|
storageHandler.SetCache(responseCache)
|
|
}
|
|
|
|
// Start disk monitor service in background (syncs disks every 5 minutes)
|
|
diskMonitor := storage.NewDiskMonitor(db, log, 5*time.Minute)
|
|
go diskMonitor.Start(context.Background())
|
|
|
|
// Start ZFS pool monitor service in background (syncs pools every 2 minutes)
|
|
zfsPoolMonitor := storage.NewZFSPoolMonitor(db, log, 2*time.Minute)
|
|
go zfsPoolMonitor.Start(context.Background())
|
|
|
|
storageGroup := protected.Group("/storage")
|
|
storageGroup.Use(requirePermission("storage", "read"))
|
|
{
|
|
storageGroup.GET("/disks", storageHandler.ListDisks)
|
|
storageGroup.POST("/disks/sync", storageHandler.SyncDisks)
|
|
storageGroup.GET("/volume-groups", storageHandler.ListVolumeGroups)
|
|
storageGroup.GET("/repositories", storageHandler.ListRepositories)
|
|
storageGroup.GET("/repositories/:id", storageHandler.GetRepository)
|
|
storageGroup.POST("/repositories", requirePermission("storage", "write"), storageHandler.CreateRepository)
|
|
storageGroup.DELETE("/repositories/:id", requirePermission("storage", "write"), storageHandler.DeleteRepository)
|
|
// ZFS Pools
|
|
storageGroup.GET("/zfs/pools", storageHandler.ListZFSPools)
|
|
storageGroup.GET("/zfs/pools/:id", storageHandler.GetZFSPool)
|
|
storageGroup.POST("/zfs/pools", requirePermission("storage", "write"), storageHandler.CreateZPool)
|
|
storageGroup.DELETE("/zfs/pools/:id", requirePermission("storage", "write"), storageHandler.DeleteZFSPool)
|
|
storageGroup.POST("/zfs/pools/:id/spare", requirePermission("storage", "write"), storageHandler.AddSpareDisk)
|
|
// ZFS Datasets
|
|
storageGroup.GET("/zfs/pools/:id/datasets", storageHandler.ListZFSDatasets)
|
|
storageGroup.POST("/zfs/pools/:id/datasets", requirePermission("storage", "write"), storageHandler.CreateZFSDataset)
|
|
storageGroup.DELETE("/zfs/pools/:id/datasets/:dataset", requirePermission("storage", "write"), storageHandler.DeleteZFSDataset)
|
|
// ZFS ARC Stats
|
|
storageGroup.GET("/zfs/arc/stats", storageHandler.GetARCStats)
|
|
}
|
|
|
|
// SCST
|
|
scstHandler := scst.NewHandler(db, log)
|
|
scstGroup := protected.Group("/scst")
|
|
scstGroup.Use(requirePermission("iscsi", "read"))
|
|
{
|
|
scstGroup.GET("/targets", scstHandler.ListTargets)
|
|
scstGroup.GET("/targets/:id", scstHandler.GetTarget)
|
|
scstGroup.POST("/targets", scstHandler.CreateTarget)
|
|
scstGroup.POST("/targets/:id/luns", scstHandler.AddLUN)
|
|
scstGroup.POST("/targets/:id/initiators", scstHandler.AddInitiator)
|
|
scstGroup.POST("/targets/:id/enable", scstHandler.EnableTarget)
|
|
scstGroup.POST("/targets/:id/disable", scstHandler.DisableTarget)
|
|
scstGroup.GET("/initiators", scstHandler.ListAllInitiators)
|
|
scstGroup.GET("/initiators/:id", scstHandler.GetInitiator)
|
|
scstGroup.DELETE("/initiators/:id", scstHandler.RemoveInitiator)
|
|
scstGroup.GET("/extents", scstHandler.ListExtents)
|
|
scstGroup.POST("/extents", scstHandler.CreateExtent)
|
|
scstGroup.DELETE("/extents/:device", scstHandler.DeleteExtent)
|
|
scstGroup.POST("/config/apply", scstHandler.ApplyConfig)
|
|
scstGroup.GET("/handlers", scstHandler.ListHandlers)
|
|
scstGroup.GET("/portals", scstHandler.ListPortals)
|
|
scstGroup.GET("/portals/:id", scstHandler.GetPortal)
|
|
scstGroup.POST("/portals", scstHandler.CreatePortal)
|
|
scstGroup.PUT("/portals/:id", scstHandler.UpdatePortal)
|
|
scstGroup.DELETE("/portals/:id", scstHandler.DeletePortal)
|
|
}
|
|
|
|
// Physical Tape Libraries
|
|
tapeHandler := tape_physical.NewHandler(db, log)
|
|
tapeGroup := protected.Group("/tape/physical")
|
|
tapeGroup.Use(requirePermission("tape", "read"))
|
|
{
|
|
tapeGroup.GET("/libraries", tapeHandler.ListLibraries)
|
|
tapeGroup.POST("/libraries/discover", tapeHandler.DiscoverLibraries)
|
|
tapeGroup.GET("/libraries/:id", tapeHandler.GetLibrary)
|
|
tapeGroup.POST("/libraries/:id/inventory", tapeHandler.PerformInventory)
|
|
tapeGroup.POST("/libraries/:id/load", tapeHandler.LoadTape)
|
|
tapeGroup.POST("/libraries/:id/unload", tapeHandler.UnloadTape)
|
|
}
|
|
|
|
// Virtual Tape Libraries
|
|
vtlHandler := tape_vtl.NewHandler(db, log)
|
|
|
|
// Start MHVTL monitor service in background (syncs every 5 minutes)
|
|
mhvtlMonitor := tape_vtl.NewMHVTLMonitor(db, log, "/etc/mhvtl", 5*time.Minute)
|
|
go mhvtlMonitor.Start(context.Background())
|
|
|
|
vtlGroup := protected.Group("/tape/vtl")
|
|
vtlGroup.Use(requirePermission("tape", "read"))
|
|
{
|
|
vtlGroup.GET("/libraries", vtlHandler.ListLibraries)
|
|
vtlGroup.POST("/libraries", vtlHandler.CreateLibrary)
|
|
vtlGroup.GET("/libraries/:id", vtlHandler.GetLibrary)
|
|
vtlGroup.DELETE("/libraries/:id", vtlHandler.DeleteLibrary)
|
|
vtlGroup.GET("/libraries/:id/drives", vtlHandler.GetLibraryDrives)
|
|
vtlGroup.GET("/libraries/:id/tapes", vtlHandler.GetLibraryTapes)
|
|
vtlGroup.POST("/libraries/:id/tapes", vtlHandler.CreateTape)
|
|
vtlGroup.POST("/libraries/:id/load", vtlHandler.LoadTape)
|
|
vtlGroup.POST("/libraries/:id/unload", vtlHandler.UnloadTape)
|
|
}
|
|
|
|
// System Management
|
|
systemHandler := system.NewHandler(log, tasks.NewEngine(db, log))
|
|
systemGroup := protected.Group("/system")
|
|
systemGroup.Use(requirePermission("system", "read"))
|
|
{
|
|
systemGroup.GET("/services", systemHandler.ListServices)
|
|
systemGroup.GET("/services/:name", systemHandler.GetServiceStatus)
|
|
systemGroup.POST("/services/:name/restart", systemHandler.RestartService)
|
|
systemGroup.GET("/services/:name/logs", systemHandler.GetServiceLogs)
|
|
systemGroup.POST("/support-bundle", systemHandler.GenerateSupportBundle)
|
|
systemGroup.GET("/interfaces", systemHandler.ListNetworkInterfaces)
|
|
}
|
|
|
|
// IAM routes - GetUser can be accessed by user viewing own profile or admin
|
|
iamHandler := iam.NewHandler(db, cfg, log)
|
|
protected.GET("/iam/users/:id", iamHandler.GetUser)
|
|
|
|
// IAM admin routes
|
|
iamGroup := protected.Group("/iam")
|
|
iamGroup.Use(requireRole("admin"))
|
|
{
|
|
iamGroup.GET("/users", iamHandler.ListUsers)
|
|
iamGroup.POST("/users", iamHandler.CreateUser)
|
|
iamGroup.PUT("/users/:id", iamHandler.UpdateUser)
|
|
iamGroup.DELETE("/users/:id", iamHandler.DeleteUser)
|
|
// Roles routes
|
|
iamGroup.GET("/roles", iamHandler.ListRoles)
|
|
iamGroup.GET("/roles/:id", iamHandler.GetRole)
|
|
iamGroup.POST("/roles", iamHandler.CreateRole)
|
|
iamGroup.PUT("/roles/:id", iamHandler.UpdateRole)
|
|
iamGroup.DELETE("/roles/:id", iamHandler.DeleteRole)
|
|
iamGroup.GET("/roles/:id/permissions", iamHandler.GetRolePermissions)
|
|
iamGroup.POST("/roles/:id/permissions", iamHandler.AssignPermissionToRole)
|
|
iamGroup.DELETE("/roles/:id/permissions", iamHandler.RemovePermissionFromRole)
|
|
|
|
// Permissions routes
|
|
iamGroup.GET("/permissions", iamHandler.ListPermissions)
|
|
|
|
// User role/group assignment
|
|
iamGroup.POST("/users/:id/roles", iamHandler.AssignRoleToUser)
|
|
iamGroup.DELETE("/users/:id/roles", iamHandler.RemoveRoleFromUser)
|
|
iamGroup.POST("/users/:id/groups", iamHandler.AssignGroupToUser)
|
|
iamGroup.DELETE("/users/:id/groups", iamHandler.RemoveGroupFromUser)
|
|
|
|
// Groups routes
|
|
iamGroup.GET("/groups", iamHandler.ListGroups)
|
|
iamGroup.GET("/groups/:id", iamHandler.GetGroup)
|
|
iamGroup.POST("/groups", iamHandler.CreateGroup)
|
|
iamGroup.PUT("/groups/:id", iamHandler.UpdateGroup)
|
|
iamGroup.DELETE("/groups/:id", iamHandler.DeleteGroup)
|
|
iamGroup.POST("/groups/:id/users", iamHandler.AddUserToGroup)
|
|
iamGroup.DELETE("/groups/:id/users/:user_id", iamHandler.RemoveUserFromGroup)
|
|
}
|
|
|
|
// Backup Jobs
|
|
backupService := backup.NewService(db, log)
|
|
// Set up direct connection to Bacula database
|
|
// Try common Bacula database names
|
|
baculaDBName := "bacula" // Default
|
|
if err := backupService.SetBaculaDatabase(cfg.Database, baculaDBName); err != nil {
|
|
log.Warn("Failed to connect to Bacula database, trying 'bareos'", "error", err)
|
|
// Try 'bareos' as alternative
|
|
if err := backupService.SetBaculaDatabase(cfg.Database, "bareos"); err != nil {
|
|
log.Error("Failed to connect to Bacula database", "error", err, "tried", []string{"bacula", "bareos"})
|
|
// Continue anyway - will fallback to bconsole
|
|
}
|
|
}
|
|
backupHandler := backup.NewHandler(backupService, log)
|
|
backupGroup := protected.Group("/backup")
|
|
backupGroup.Use(requirePermission("backup", "read"))
|
|
{
|
|
backupGroup.GET("/jobs", backupHandler.ListJobs)
|
|
backupGroup.GET("/jobs/:id", backupHandler.GetJob)
|
|
backupGroup.POST("/jobs", requirePermission("backup", "write"), backupHandler.CreateJob)
|
|
}
|
|
|
|
// Monitoring
|
|
monitoringHandler := monitoring.NewHandler(db, log, alertService, metricsService, eventHub)
|
|
monitoringGroup := protected.Group("/monitoring")
|
|
monitoringGroup.Use(requirePermission("monitoring", "read"))
|
|
{
|
|
// Alerts
|
|
monitoringGroup.GET("/alerts", monitoringHandler.ListAlerts)
|
|
monitoringGroup.GET("/alerts/:id", monitoringHandler.GetAlert)
|
|
monitoringGroup.POST("/alerts/:id/acknowledge", monitoringHandler.AcknowledgeAlert)
|
|
monitoringGroup.POST("/alerts/:id/resolve", monitoringHandler.ResolveAlert)
|
|
|
|
// Metrics
|
|
monitoringGroup.GET("/metrics", monitoringHandler.GetMetrics)
|
|
|
|
// WebSocket (no permission check needed, handled by auth middleware)
|
|
monitoringGroup.GET("/events", monitoringHandler.WebSocketHandler)
|
|
}
|
|
}
|
|
}
|
|
|
|
return r
|
|
}
|
|
|
|
// ginLogger creates a Gin middleware for logging
|
|
func ginLogger(log *logger.Logger) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
c.Next()
|
|
|
|
log.Info("HTTP request",
|
|
"method", c.Request.Method,
|
|
"path", c.Request.URL.Path,
|
|
"status", c.Writer.Status(),
|
|
"client_ip", c.ClientIP(),
|
|
"latency_ms", c.Writer.Size(),
|
|
)
|
|
}
|
|
}
|