Files
calypso/backend/internal/scst/handler.go
2025-12-29 02:44:52 +07:00

477 lines
14 KiB
Go

package scst
import (
"fmt"
"net/http"
"github.com/atlasos/calypso/internal/common/database"
"github.com/atlasos/calypso/internal/common/logger"
"github.com/atlasos/calypso/internal/tasks"
"github.com/gin-gonic/gin"
)
// Handler handles SCST-related API requests
type Handler struct {
service *Service
taskEngine *tasks.Engine
db *database.DB
logger *logger.Logger
}
// NewHandler creates a new SCST handler
func NewHandler(db *database.DB, log *logger.Logger) *Handler {
return &Handler{
service: NewService(db, log),
taskEngine: tasks.NewEngine(db, log),
db: db,
logger: log,
}
}
// ListTargets lists all SCST targets
func (h *Handler) ListTargets(c *gin.Context) {
targets, err := h.service.ListTargets(c.Request.Context())
if err != nil {
h.logger.Error("Failed to list targets", "error", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to list targets"})
return
}
c.JSON(http.StatusOK, gin.H{"targets": targets})
}
// GetTarget retrieves a target by ID
func (h *Handler) GetTarget(c *gin.Context) {
targetID := c.Param("id")
target, err := h.service.GetTarget(c.Request.Context(), targetID)
if err != nil {
if err.Error() == "target not found" {
c.JSON(http.StatusNotFound, gin.H{"error": "target not found"})
return
}
h.logger.Error("Failed to get target", "error", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to get target"})
return
}
// Get LUNs
luns, err := h.service.GetTargetLUNs(c.Request.Context(), targetID)
if err != nil {
h.logger.Warn("Failed to get LUNs", "target_id", targetID, "error", err)
// Return empty array instead of nil
luns = []LUN{}
}
// Get initiator groups
groups, err2 := h.service.GetTargetInitiatorGroups(c.Request.Context(), targetID)
if err2 != nil {
h.logger.Warn("Failed to get initiator groups", "target_id", targetID, "error", err2)
groups = []InitiatorGroup{}
}
c.JSON(http.StatusOK, gin.H{
"target": target,
"luns": luns,
"initiator_groups": groups,
})
}
// CreateTargetRequest represents a target creation request
type CreateTargetRequest struct {
IQN string `json:"iqn" binding:"required"`
TargetType string `json:"target_type" binding:"required"`
Name string `json:"name" binding:"required"`
Description string `json:"description"`
SingleInitiatorOnly bool `json:"single_initiator_only"`
}
// CreateTarget creates a new SCST target
func (h *Handler) CreateTarget(c *gin.Context) {
var req CreateTargetRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request"})
return
}
userID, _ := c.Get("user_id")
target := &Target{
IQN: req.IQN,
TargetType: req.TargetType,
Name: req.Name,
Description: req.Description,
IsActive: true,
SingleInitiatorOnly: req.SingleInitiatorOnly || req.TargetType == "vtl" || req.TargetType == "physical_tape",
CreatedBy: userID.(string),
}
if err := h.service.CreateTarget(c.Request.Context(), target); err != nil {
h.logger.Error("Failed to create target", "error", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, target)
}
// AddLUNRequest represents a LUN addition request
type AddLUNRequest struct {
DeviceName string `json:"device_name" binding:"required"`
DevicePath string `json:"device_path" binding:"required"`
LUNNumber int `json:"lun_number" binding:"required"`
HandlerType string `json:"handler_type" binding:"required"`
}
// AddLUN adds a LUN to a target
func (h *Handler) AddLUN(c *gin.Context) {
targetID := c.Param("id")
target, err := h.service.GetTarget(c.Request.Context(), targetID)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "target not found"})
return
}
var req AddLUNRequest
if err := c.ShouldBindJSON(&req); err != nil {
h.logger.Error("Failed to bind AddLUN request", "error", err)
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("invalid request: %v", err)})
return
}
// Validate required fields
if req.DeviceName == "" || req.DevicePath == "" || req.HandlerType == "" {
h.logger.Error("Missing required fields in AddLUN request", "device_name", req.DeviceName, "device_path", req.DevicePath, "handler_type", req.HandlerType)
c.JSON(http.StatusBadRequest, gin.H{"error": "device_name, device_path, and handler_type are required"})
return
}
if err := h.service.AddLUN(c.Request.Context(), target.IQN, req.DeviceName, req.DevicePath, req.LUNNumber, req.HandlerType); err != nil {
h.logger.Error("Failed to add LUN", "error", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "LUN added successfully"})
}
// AddInitiatorRequest represents an initiator addition request
type AddInitiatorRequest struct {
InitiatorIQN string `json:"initiator_iqn" binding:"required"`
}
// AddInitiator adds an initiator to a target
func (h *Handler) AddInitiator(c *gin.Context) {
targetID := c.Param("id")
target, err := h.service.GetTarget(c.Request.Context(), targetID)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "target not found"})
return
}
var req AddInitiatorRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request"})
return
}
if err := h.service.AddInitiator(c.Request.Context(), target.IQN, req.InitiatorIQN); err != nil {
h.logger.Error("Failed to add initiator", "error", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Initiator added successfully"})
}
// ListAllInitiators lists all initiators across all targets
func (h *Handler) ListAllInitiators(c *gin.Context) {
initiators, err := h.service.ListAllInitiators(c.Request.Context())
if err != nil {
h.logger.Error("Failed to list initiators", "error", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to list initiators"})
return
}
if initiators == nil {
initiators = []InitiatorWithTarget{}
}
c.JSON(http.StatusOK, gin.H{"initiators": initiators})
}
// RemoveInitiator removes an initiator
func (h *Handler) RemoveInitiator(c *gin.Context) {
initiatorID := c.Param("id")
if err := h.service.RemoveInitiator(c.Request.Context(), initiatorID); err != nil {
if err.Error() == "initiator not found" {
c.JSON(http.StatusNotFound, gin.H{"error": "initiator not found"})
return
}
h.logger.Error("Failed to remove initiator", "error", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Initiator removed successfully"})
}
// GetInitiator retrieves an initiator by ID
func (h *Handler) GetInitiator(c *gin.Context) {
initiatorID := c.Param("id")
initiator, err := h.service.GetInitiator(c.Request.Context(), initiatorID)
if err != nil {
if err.Error() == "initiator not found" {
c.JSON(http.StatusNotFound, gin.H{"error": "initiator not found"})
return
}
h.logger.Error("Failed to get initiator", "error", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to get initiator"})
return
}
c.JSON(http.StatusOK, initiator)
}
// ListExtents lists all device extents
func (h *Handler) ListExtents(c *gin.Context) {
extents, err := h.service.ListExtents(c.Request.Context())
if err != nil {
h.logger.Error("Failed to list extents", "error", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to list extents"})
return
}
if extents == nil {
extents = []Extent{}
}
c.JSON(http.StatusOK, gin.H{"extents": extents})
}
// CreateExtentRequest represents a request to create an extent
type CreateExtentRequest struct {
DeviceName string `json:"device_name" binding:"required"`
DevicePath string `json:"device_path" binding:"required"`
HandlerType string `json:"handler_type" binding:"required"`
}
// CreateExtent creates a new device extent
func (h *Handler) CreateExtent(c *gin.Context) {
var req CreateExtentRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request"})
return
}
if err := h.service.CreateExtent(c.Request.Context(), req.DeviceName, req.DevicePath, req.HandlerType); err != nil {
h.logger.Error("Failed to create extent", "error", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, gin.H{"message": "Extent created successfully"})
}
// DeleteExtent deletes a device extent
func (h *Handler) DeleteExtent(c *gin.Context) {
deviceName := c.Param("device")
if err := h.service.DeleteExtent(c.Request.Context(), deviceName); err != nil {
h.logger.Error("Failed to delete extent", "error", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Extent deleted successfully"})
}
// ApplyConfig applies SCST configuration
func (h *Handler) ApplyConfig(c *gin.Context) {
userID, _ := c.Get("user_id")
// Create async task
taskID, err := h.taskEngine.CreateTask(c.Request.Context(),
tasks.TaskTypeApplySCST, userID.(string), map[string]interface{}{
"operation": "apply_scst_config",
})
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create task"})
return
}
// Run apply in background
go func() {
ctx := c.Request.Context()
h.taskEngine.StartTask(ctx, taskID)
h.taskEngine.UpdateProgress(ctx, taskID, 50, "Writing SCST configuration...")
configPath := "/etc/calypso/scst/generated.conf"
if err := h.service.WriteConfig(ctx, configPath); err != nil {
h.taskEngine.FailTask(ctx, taskID, err.Error())
return
}
h.taskEngine.UpdateProgress(ctx, taskID, 100, "SCST configuration applied")
h.taskEngine.CompleteTask(ctx, taskID, "SCST configuration applied successfully")
}()
c.JSON(http.StatusAccepted, gin.H{"task_id": taskID})
}
// ListHandlers lists available SCST handlers
func (h *Handler) ListHandlers(c *gin.Context) {
handlers, err := h.service.DetectHandlers(c.Request.Context())
if err != nil {
h.logger.Error("Failed to list handlers", "error", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to list handlers"})
return
}
c.JSON(http.StatusOK, gin.H{"handlers": handlers})
}
// ListPortals lists all iSCSI portals
func (h *Handler) ListPortals(c *gin.Context) {
portals, err := h.service.ListPortals(c.Request.Context())
if err != nil {
h.logger.Error("Failed to list portals", "error", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to list portals"})
return
}
// Ensure we return an empty array instead of null
if portals == nil {
portals = []Portal{}
}
c.JSON(http.StatusOK, gin.H{"portals": portals})
}
// CreatePortal creates a new portal
func (h *Handler) CreatePortal(c *gin.Context) {
var portal Portal
if err := c.ShouldBindJSON(&portal); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request"})
return
}
if err := h.service.CreatePortal(c.Request.Context(), &portal); err != nil {
h.logger.Error("Failed to create portal", "error", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, portal)
}
// UpdatePortal updates a portal
func (h *Handler) UpdatePortal(c *gin.Context) {
id := c.Param("id")
var portal Portal
if err := c.ShouldBindJSON(&portal); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request"})
return
}
if err := h.service.UpdatePortal(c.Request.Context(), id, &portal); err != nil {
if err.Error() == "portal not found" {
c.JSON(http.StatusNotFound, gin.H{"error": "portal not found"})
return
}
h.logger.Error("Failed to update portal", "error", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, portal)
}
// EnableTarget enables a target
func (h *Handler) EnableTarget(c *gin.Context) {
targetID := c.Param("id")
target, err := h.service.GetTarget(c.Request.Context(), targetID)
if err != nil {
if err.Error() == "target not found" {
c.JSON(http.StatusNotFound, gin.H{"error": "target not found"})
return
}
h.logger.Error("Failed to get target", "error", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to get target"})
return
}
if err := h.service.EnableTarget(c.Request.Context(), target.IQN); err != nil {
h.logger.Error("Failed to enable target", "error", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Target enabled successfully"})
}
// DisableTarget disables a target
func (h *Handler) DisableTarget(c *gin.Context) {
targetID := c.Param("id")
target, err := h.service.GetTarget(c.Request.Context(), targetID)
if err != nil {
if err.Error() == "target not found" {
c.JSON(http.StatusNotFound, gin.H{"error": "target not found"})
return
}
h.logger.Error("Failed to get target", "error", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to get target"})
return
}
if err := h.service.DisableTarget(c.Request.Context(), target.IQN); err != nil {
h.logger.Error("Failed to disable target", "error", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Target disabled successfully"})
}
// DeletePortal deletes a portal
func (h *Handler) DeletePortal(c *gin.Context) {
id := c.Param("id")
if err := h.service.DeletePortal(c.Request.Context(), id); err != nil {
if err.Error() == "portal not found" {
c.JSON(http.StatusNotFound, gin.H{"error": "portal not found"})
return
}
h.logger.Error("Failed to delete portal", "error", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Portal deleted successfully"})
}
// GetPortal retrieves a portal by ID
func (h *Handler) GetPortal(c *gin.Context) {
id := c.Param("id")
portal, err := h.service.GetPortal(c.Request.Context(), id)
if err != nil {
if err.Error() == "portal not found" {
c.JSON(http.StatusNotFound, gin.H{"error": "portal not found"})
return
}
h.logger.Error("Failed to get portal", "error", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to get portal"})
return
}
c.JSON(http.StatusOK, portal)
}