working on storage dashboard
This commit is contained in:
@@ -1,7 +1,9 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/atlasos/calypso/internal/common/database"
|
||||
"github.com/atlasos/calypso/internal/common/logger"
|
||||
@@ -13,6 +15,7 @@ import (
|
||||
type Handler struct {
|
||||
diskService *DiskService
|
||||
lvmService *LVMService
|
||||
zfsService *ZFSService
|
||||
taskEngine *tasks.Engine
|
||||
db *database.DB
|
||||
logger *logger.Logger
|
||||
@@ -23,6 +26,7 @@ func NewHandler(db *database.DB, log *logger.Logger) *Handler {
|
||||
return &Handler{
|
||||
diskService: NewDiskService(db, log),
|
||||
lvmService: NewLVMService(db, log),
|
||||
zfsService: NewZFSService(db, log),
|
||||
taskEngine: tasks.NewEngine(db, log),
|
||||
db: db,
|
||||
logger: log,
|
||||
@@ -167,3 +171,288 @@ func (h *Handler) DeleteRepository(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"message": "repository deleted successfully"})
|
||||
}
|
||||
|
||||
// CreateZPoolRequest represents a ZFS pool creation request
|
||||
type CreateZPoolRequest struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
Description string `json:"description"`
|
||||
RaidLevel string `json:"raid_level" binding:"required"` // stripe, mirror, raidz, raidz2, raidz3
|
||||
Disks []string `json:"disks" binding:"required"` // device paths
|
||||
Compression string `json:"compression"` // off, lz4, zstd, gzip
|
||||
Deduplication bool `json:"deduplication"`
|
||||
AutoExpand bool `json:"auto_expand"`
|
||||
}
|
||||
|
||||
// CreateZPool creates a new ZFS pool
|
||||
func (h *Handler) CreateZPool(c *gin.Context) {
|
||||
var req CreateZPoolRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
h.logger.Error("Invalid request body", "error", err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Validate required fields
|
||||
if req.Name == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "pool name is required"})
|
||||
return
|
||||
}
|
||||
if req.RaidLevel == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "raid_level is required"})
|
||||
return
|
||||
}
|
||||
if len(req.Disks) == 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "at least one disk is required"})
|
||||
return
|
||||
}
|
||||
|
||||
userID, exists := c.Get("user_id")
|
||||
if !exists {
|
||||
h.logger.Error("User ID not found in context")
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "authentication required"})
|
||||
return
|
||||
}
|
||||
|
||||
userIDStr, ok := userID.(string)
|
||||
if !ok {
|
||||
h.logger.Error("Invalid user ID type", "type", fmt.Sprintf("%T", userID))
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "invalid user context"})
|
||||
return
|
||||
}
|
||||
|
||||
// Set default compression if not provided
|
||||
if req.Compression == "" {
|
||||
req.Compression = "lz4"
|
||||
}
|
||||
|
||||
h.logger.Info("Creating ZFS pool request", "name", req.Name, "raid_level", req.RaidLevel, "disks", req.Disks, "compression", req.Compression)
|
||||
|
||||
pool, err := h.zfsService.CreatePool(
|
||||
c.Request.Context(),
|
||||
req.Name,
|
||||
req.RaidLevel,
|
||||
req.Disks,
|
||||
req.Compression,
|
||||
req.Deduplication,
|
||||
req.AutoExpand,
|
||||
userIDStr,
|
||||
)
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to create ZFS pool", "error", err, "name", req.Name, "raid_level", req.RaidLevel, "disks", req.Disks)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Info("ZFS pool created successfully", "pool_id", pool.ID, "name", pool.Name)
|
||||
c.JSON(http.StatusCreated, pool)
|
||||
}
|
||||
|
||||
// ListZFSPools lists all ZFS pools
|
||||
func (h *Handler) ListZFSPools(c *gin.Context) {
|
||||
pools, err := h.zfsService.ListPools(c.Request.Context())
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to list ZFS pools", "error", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to list ZFS pools"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"pools": pools})
|
||||
}
|
||||
|
||||
// GetZFSPool retrieves a ZFS pool by ID
|
||||
func (h *Handler) GetZFSPool(c *gin.Context) {
|
||||
poolID := c.Param("id")
|
||||
|
||||
pool, err := h.zfsService.GetPool(c.Request.Context(), poolID)
|
||||
if err != nil {
|
||||
if err.Error() == "pool not found" {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "pool not found"})
|
||||
return
|
||||
}
|
||||
h.logger.Error("Failed to get ZFS pool", "error", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to get ZFS pool"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, pool)
|
||||
}
|
||||
|
||||
// DeleteZFSPool deletes a ZFS pool
|
||||
func (h *Handler) DeleteZFSPool(c *gin.Context) {
|
||||
poolID := c.Param("id")
|
||||
|
||||
if err := h.zfsService.DeletePool(c.Request.Context(), poolID); err != nil {
|
||||
if err.Error() == "pool not found" {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "pool not found"})
|
||||
return
|
||||
}
|
||||
h.logger.Error("Failed to delete ZFS pool", "error", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "ZFS pool deleted successfully"})
|
||||
}
|
||||
|
||||
// AddSpareDiskRequest represents a request to add spare disks to a pool
|
||||
type AddSpareDiskRequest struct {
|
||||
Disks []string `json:"disks" binding:"required"`
|
||||
}
|
||||
|
||||
// AddSpareDisk adds spare disks to a ZFS pool
|
||||
func (h *Handler) AddSpareDisk(c *gin.Context) {
|
||||
poolID := c.Param("id")
|
||||
|
||||
var req AddSpareDiskRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
h.logger.Error("Invalid add spare disk request", "error", err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if len(req.Disks) == 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "at least one disk must be specified"})
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.zfsService.AddSpareDisk(c.Request.Context(), poolID, req.Disks); err != nil {
|
||||
if err.Error() == "pool not found" {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "pool not found"})
|
||||
return
|
||||
}
|
||||
h.logger.Error("Failed to add spare disks", "error", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Spare disks added successfully"})
|
||||
}
|
||||
|
||||
// ListZFSDatasets lists all datasets in a ZFS pool
|
||||
func (h *Handler) ListZFSDatasets(c *gin.Context) {
|
||||
poolID := c.Param("id")
|
||||
|
||||
// Get pool to get pool name
|
||||
pool, err := h.zfsService.GetPool(c.Request.Context(), poolID)
|
||||
if err != nil {
|
||||
if err.Error() == "pool not found" {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "pool not found"})
|
||||
return
|
||||
}
|
||||
h.logger.Error("Failed to get pool", "error", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to get pool"})
|
||||
return
|
||||
}
|
||||
|
||||
datasets, err := h.zfsService.ListDatasets(c.Request.Context(), pool.Name)
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to list datasets", "error", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"datasets": datasets})
|
||||
}
|
||||
|
||||
// CreateZFSDatasetRequest represents a request to create a ZFS dataset
|
||||
type CreateZFSDatasetRequest struct {
|
||||
Name string `json:"name" binding:"required"` // Dataset name (without pool prefix)
|
||||
Type string `json:"type" binding:"required"` // "filesystem" or "volume"
|
||||
Compression string `json:"compression"` // off, lz4, zstd, gzip, etc.
|
||||
Quota int64 `json:"quota"` // -1 for unlimited, >0 for size
|
||||
Reservation int64 `json:"reservation"` // 0 for none
|
||||
MountPoint string `json:"mount_point"` // Optional mount point
|
||||
}
|
||||
|
||||
// CreateZFSDataset creates a new ZFS dataset in a pool
|
||||
func (h *Handler) CreateZFSDataset(c *gin.Context) {
|
||||
poolID := c.Param("id")
|
||||
|
||||
// Get pool to get pool name
|
||||
pool, err := h.zfsService.GetPool(c.Request.Context(), poolID)
|
||||
if err != nil {
|
||||
if err.Error() == "pool not found" {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "pool not found"})
|
||||
return
|
||||
}
|
||||
h.logger.Error("Failed to get pool", "error", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to get pool"})
|
||||
return
|
||||
}
|
||||
|
||||
var req CreateZFSDatasetRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
h.logger.Error("Invalid create dataset request", "error", err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Validate type
|
||||
if req.Type != "filesystem" && req.Type != "volume" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "type must be 'filesystem' or 'volume'"})
|
||||
return
|
||||
}
|
||||
|
||||
// Validate dataset name (should not contain pool name)
|
||||
if strings.Contains(req.Name, "/") {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "dataset name should not contain '/' (pool name is automatically prepended)"})
|
||||
return
|
||||
}
|
||||
|
||||
// Create dataset request - CreateDatasetRequest is in the same package (zfs.go)
|
||||
createReq := CreateDatasetRequest{
|
||||
Name: req.Name,
|
||||
Type: req.Type,
|
||||
Compression: req.Compression,
|
||||
Quota: req.Quota,
|
||||
Reservation: req.Reservation,
|
||||
MountPoint: req.MountPoint,
|
||||
}
|
||||
|
||||
dataset, err := h.zfsService.CreateDataset(c.Request.Context(), pool.Name, createReq)
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to create dataset", "error", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, dataset)
|
||||
}
|
||||
|
||||
// DeleteZFSDataset deletes a ZFS dataset
|
||||
func (h *Handler) DeleteZFSDataset(c *gin.Context) {
|
||||
poolID := c.Param("id")
|
||||
datasetName := c.Param("dataset")
|
||||
|
||||
// Get pool to get pool name
|
||||
pool, err := h.zfsService.GetPool(c.Request.Context(), poolID)
|
||||
if err != nil {
|
||||
if err.Error() == "pool not found" {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "pool not found"})
|
||||
return
|
||||
}
|
||||
h.logger.Error("Failed to get pool", "error", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to get pool"})
|
||||
return
|
||||
}
|
||||
|
||||
// Construct full dataset name
|
||||
fullDatasetName := pool.Name + "/" + datasetName
|
||||
|
||||
// Verify dataset belongs to this pool
|
||||
if !strings.HasPrefix(fullDatasetName, pool.Name+"/") {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "dataset does not belong to this pool"})
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.zfsService.DeleteDataset(c.Request.Context(), fullDatasetName); err != nil {
|
||||
if strings.Contains(err.Error(), "does not exist") || strings.Contains(err.Error(), "not found") {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "dataset not found"})
|
||||
return
|
||||
}
|
||||
h.logger.Error("Failed to delete dataset", "error", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Dataset deleted successfully"})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user