fix storage

This commit is contained in:
2026-01-09 16:54:39 +00:00
parent dcb54c26ec
commit 7b91e0fd24
37 changed files with 3283 additions and 1227 deletions

View File

@@ -16,6 +16,16 @@ import (
"github.com/lib/pq"
)
// zfsCommand executes a ZFS command with sudo
func zfsCommand(ctx context.Context, args ...string) *exec.Cmd {
return exec.CommandContext(ctx, "sudo", append([]string{"zfs"}, args...)...)
}
// zpoolCommand executes a ZPOOL command with sudo
func zpoolCommand(ctx context.Context, args ...string) *exec.Cmd {
return exec.CommandContext(ctx, "sudo", append([]string{"zpool"}, args...)...)
}
// ZFSService handles ZFS pool management
type ZFSService struct {
db *database.DB
@@ -115,6 +125,10 @@ func (s *ZFSService) CreatePool(ctx context.Context, name string, raidLevel stri
var args []string
args = append(args, "create", "-f") // -f to force creation
// Set default mountpoint to /opt/calypso/data/pool/<pool-name>
mountPoint := fmt.Sprintf("/opt/calypso/data/pool/%s", name)
args = append(args, "-m", mountPoint)
// Note: compression is a filesystem property, not a pool property
// We'll set it after pool creation using zfs set
@@ -155,9 +169,15 @@ func (s *ZFSService) CreatePool(ctx context.Context, name string, raidLevel stri
args = append(args, disks...)
}
// Execute zpool create
s.logger.Info("Creating ZFS pool", "name", name, "raid_level", raidLevel, "disks", disks, "args", args)
cmd := exec.CommandContext(ctx, "zpool", args...)
// Create mountpoint directory if it doesn't exist
if err := os.MkdirAll(mountPoint, 0755); err != nil {
return nil, fmt.Errorf("failed to create mountpoint directory %s: %w", mountPoint, err)
}
s.logger.Info("Created mountpoint directory", "path", mountPoint)
// Execute zpool create (with sudo for permissions)
s.logger.Info("Creating ZFS pool", "name", name, "raid_level", raidLevel, "disks", disks, "mountpoint", mountPoint, "args", args)
cmd := zpoolCommand(ctx, args...)
output, err := cmd.CombinedOutput()
if err != nil {
errorMsg := string(output)
@@ -170,7 +190,7 @@ func (s *ZFSService) CreatePool(ctx context.Context, name string, raidLevel stri
// Set filesystem properties (compression, etc.) after pool creation
// ZFS creates a root filesystem with the same name as the pool
if compression != "" && compression != "off" {
cmd = exec.CommandContext(ctx, "zfs", "set", fmt.Sprintf("compression=%s", compression), name)
cmd = zfsCommand(ctx, "set", fmt.Sprintf("compression=%s", compression), name)
output, err = cmd.CombinedOutput()
if err != nil {
s.logger.Warn("Failed to set compression property", "pool", name, "compression", compression, "error", string(output))
@@ -185,7 +205,7 @@ func (s *ZFSService) CreatePool(ctx context.Context, name string, raidLevel stri
if err != nil {
// Try to destroy the pool if we can't get info
s.logger.Warn("Failed to get pool info, attempting to destroy pool", "name", name, "error", err)
exec.CommandContext(ctx, "zpool", "destroy", "-f", name).Run()
zpoolCommand(ctx, "destroy", "-f", name).Run()
return nil, fmt.Errorf("failed to get pool info after creation: %w", err)
}
@@ -219,7 +239,7 @@ func (s *ZFSService) CreatePool(ctx context.Context, name string, raidLevel stri
if err != nil {
// Cleanup: destroy pool if database insert fails
s.logger.Error("Failed to save pool to database, destroying pool", "name", name, "error", err)
exec.CommandContext(ctx, "zpool", "destroy", "-f", name).Run()
zpoolCommand(ctx, "destroy", "-f", name).Run()
return nil, fmt.Errorf("failed to save pool to database: %w", err)
}
@@ -243,7 +263,7 @@ func (s *ZFSService) CreatePool(ctx context.Context, name string, raidLevel stri
// getPoolInfo retrieves information about a ZFS pool
func (s *ZFSService) getPoolInfo(ctx context.Context, poolName string) (*ZFSPool, error) {
// Get pool size and used space
cmd := exec.CommandContext(ctx, "zpool", "list", "-H", "-o", "name,size,allocated", poolName)
cmd := zpoolCommand(ctx, "list", "-H", "-o", "name,size,allocated", poolName)
output, err := cmd.CombinedOutput()
if err != nil {
errorMsg := string(output)
@@ -322,7 +342,7 @@ func parseZFSSize(sizeStr string) (int64, error) {
// getSpareDisks retrieves spare disks from zpool status
func (s *ZFSService) getSpareDisks(ctx context.Context, poolName string) ([]string, error) {
cmd := exec.CommandContext(ctx, "zpool", "status", poolName)
cmd := zpoolCommand(ctx, "status", poolName)
output, err := cmd.CombinedOutput()
if err != nil {
return nil, fmt.Errorf("failed to get pool status: %w", err)
@@ -363,7 +383,7 @@ func (s *ZFSService) getSpareDisks(ctx context.Context, poolName string) ([]stri
// getCompressRatio gets the compression ratio from ZFS
func (s *ZFSService) getCompressRatio(ctx context.Context, poolName string) (float64, error) {
cmd := exec.CommandContext(ctx, "zfs", "get", "-H", "-o", "value", "compressratio", poolName)
cmd := zfsCommand(ctx, "get", "-H", "-o", "value", "compressratio", poolName)
output, err := cmd.Output()
if err != nil {
return 1.0, err
@@ -406,16 +426,20 @@ func (s *ZFSService) ListPools(ctx context.Context) ([]*ZFSPool, error) {
for rows.Next() {
var pool ZFSPool
var description sql.NullString
var createdBy sql.NullString
err := rows.Scan(
&pool.ID, &pool.Name, &description, &pool.RaidLevel, pq.Array(&pool.Disks),
&pool.SizeBytes, &pool.UsedBytes, &pool.Compression, &pool.Deduplication,
&pool.AutoExpand, &pool.ScrubInterval, &pool.IsActive, &pool.HealthStatus,
&pool.CreatedAt, &pool.UpdatedAt, &pool.CreatedBy,
&pool.CreatedAt, &pool.UpdatedAt, &createdBy,
)
if err != nil {
s.logger.Error("Failed to scan pool row", "error", err)
s.logger.Error("Failed to scan pool row", "error", err, "error_type", fmt.Sprintf("%T", err))
continue // Skip this pool instead of failing entire query
}
if createdBy.Valid {
pool.CreatedBy = createdBy.String
}
if description.Valid {
pool.Description = description.String
}
@@ -501,7 +525,7 @@ func (s *ZFSService) DeletePool(ctx context.Context, poolID string) error {
// Destroy ZFS pool with -f flag to force destroy (works for both empty and non-empty pools)
// The -f flag is needed to destroy pools even if they have datasets or are in use
s.logger.Info("Destroying ZFS pool", "pool", pool.Name)
cmd := exec.CommandContext(ctx, "zpool", "destroy", "-f", pool.Name)
cmd := zpoolCommand(ctx, "destroy", "-f", pool.Name)
output, err := cmd.CombinedOutput()
if err != nil {
errorMsg := string(output)
@@ -516,6 +540,15 @@ func (s *ZFSService) DeletePool(ctx context.Context, poolID string) error {
s.logger.Info("ZFS pool destroyed successfully", "pool", pool.Name)
}
// Remove mount point directory (default: /opt/calypso/data/pool/<pool-name>)
mountPoint := fmt.Sprintf("/opt/calypso/data/pool/%s", pool.Name)
if err := os.RemoveAll(mountPoint); err != nil {
s.logger.Warn("Failed to remove mount point directory", "mountpoint", mountPoint, "error", err)
// Don't fail pool deletion if mount point removal fails
} else {
s.logger.Info("Removed mount point directory", "mountpoint", mountPoint)
}
// Mark disks as unused
for _, diskPath := range pool.Disks {
_, err = s.db.ExecContext(ctx,
@@ -550,7 +583,7 @@ func (s *ZFSService) AddSpareDisk(ctx context.Context, poolID string, diskPaths
}
// Verify pool exists in ZFS and check if disks are already spare
cmd := exec.CommandContext(ctx, "zpool", "status", pool.Name)
cmd := zpoolCommand(ctx, "status", pool.Name)
output, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("pool %s does not exist in ZFS: %w", pool.Name, err)
@@ -575,7 +608,7 @@ func (s *ZFSService) AddSpareDisk(ctx context.Context, poolID string, diskPaths
// Execute zpool add
s.logger.Info("Adding spare disks to ZFS pool", "pool", pool.Name, "disks", diskPaths)
cmd = exec.CommandContext(ctx, "zpool", args...)
cmd = zpoolCommand(ctx, args...)
output, err = cmd.CombinedOutput()
if err != nil {
errorMsg := string(output)
@@ -756,7 +789,7 @@ func (s *ZFSService) CreateDataset(ctx context.Context, poolName string, req Cre
// Execute zfs create
s.logger.Info("Creating ZFS dataset", "name", fullName, "type", req.Type)
cmd := exec.CommandContext(ctx, "zfs", args...)
cmd := zfsCommand(ctx, args...)
output, err := cmd.CombinedOutput()
if err != nil {
errorMsg := string(output)
@@ -766,7 +799,7 @@ func (s *ZFSService) CreateDataset(ctx context.Context, poolName string, req Cre
// Set quota if specified (for filesystems)
if req.Type == "filesystem" && req.Quota > 0 {
quotaCmd := exec.CommandContext(ctx, "zfs", "set", fmt.Sprintf("quota=%d", req.Quota), fullName)
quotaCmd := zfsCommand(ctx, "set", fmt.Sprintf("quota=%d", req.Quota), fullName)
if quotaOutput, err := quotaCmd.CombinedOutput(); err != nil {
s.logger.Warn("Failed to set quota", "dataset", fullName, "error", err, "output", string(quotaOutput))
}
@@ -774,7 +807,7 @@ func (s *ZFSService) CreateDataset(ctx context.Context, poolName string, req Cre
// Set reservation if specified
if req.Reservation > 0 {
resvCmd := exec.CommandContext(ctx, "zfs", "set", fmt.Sprintf("reservation=%d", req.Reservation), fullName)
resvCmd := zfsCommand(ctx, "set", fmt.Sprintf("reservation=%d", req.Reservation), fullName)
if resvOutput, err := resvCmd.CombinedOutput(); err != nil {
s.logger.Warn("Failed to set reservation", "dataset", fullName, "error", err, "output", string(resvOutput))
}
@@ -786,30 +819,30 @@ func (s *ZFSService) CreateDataset(ctx context.Context, poolName string, req Cre
if err != nil {
s.logger.Error("Failed to get pool ID", "pool", poolName, "error", err)
// Try to destroy the dataset if we can't save to database
exec.CommandContext(ctx, "zfs", "destroy", "-r", fullName).Run()
zfsCommand(ctx, "destroy", "-r", fullName).Run()
return nil, fmt.Errorf("failed to get pool ID: %w", err)
}
// Get dataset info from ZFS to save to database
cmd = exec.CommandContext(ctx, "zfs", "list", "-H", "-o", "name,used,avail,refer,compress,dedup,quota,reservation,mountpoint", fullName)
cmd = zfsCommand(ctx, "list", "-H", "-o", "name,used,avail,refer,compress,dedup,quota,reservation,mountpoint", fullName)
output, err = cmd.CombinedOutput()
if err != nil {
s.logger.Error("Failed to get dataset info", "name", fullName, "error", err)
// Try to destroy the dataset if we can't get info
exec.CommandContext(ctx, "zfs", "destroy", "-r", fullName).Run()
zfsCommand(ctx, "destroy", "-r", fullName).Run()
return nil, fmt.Errorf("failed to get dataset info: %w", err)
}
// Parse dataset info
lines := strings.TrimSpace(string(output))
if lines == "" {
exec.CommandContext(ctx, "zfs", "destroy", "-r", fullName).Run()
zfsCommand(ctx, "destroy", "-r", fullName).Run()
return nil, fmt.Errorf("dataset not found after creation")
}
fields := strings.Fields(lines)
if len(fields) < 9 {
exec.CommandContext(ctx, "zfs", "destroy", "-r", fullName).Run()
zfsCommand(ctx, "destroy", "-r", fullName).Run()
return nil, fmt.Errorf("invalid dataset info format")
}
@@ -824,7 +857,7 @@ func (s *ZFSService) CreateDataset(ctx context.Context, poolName string, req Cre
// Determine dataset type
datasetType := req.Type
typeCmd := exec.CommandContext(ctx, "zfs", "get", "-H", "-o", "value", "type", fullName)
typeCmd := zfsCommand(ctx, "get", "-H", "-o", "value", "type", fullName)
if typeOutput, err := typeCmd.Output(); err == nil {
volType := strings.TrimSpace(string(typeOutput))
if volType == "volume" {
@@ -838,7 +871,7 @@ func (s *ZFSService) CreateDataset(ctx context.Context, poolName string, req Cre
quota := int64(-1)
if datasetType == "volume" {
// For volumes, get volsize
volsizeCmd := exec.CommandContext(ctx, "zfs", "get", "-H", "-o", "value", "volsize", fullName)
volsizeCmd := zfsCommand(ctx, "get", "-H", "-o", "value", "volsize", fullName)
if volsizeOutput, err := volsizeCmd.Output(); err == nil {
volsizeStr := strings.TrimSpace(string(volsizeOutput))
if volsizeStr != "-" && volsizeStr != "none" {
@@ -868,7 +901,7 @@ func (s *ZFSService) CreateDataset(ctx context.Context, poolName string, req Cre
// Get creation time
createdAt := time.Now()
creationCmd := exec.CommandContext(ctx, "zfs", "get", "-H", "-o", "value", "creation", fullName)
creationCmd := zfsCommand(ctx, "get", "-H", "-o", "value", "creation", fullName)
if creationOutput, err := creationCmd.Output(); err == nil {
creationStr := strings.TrimSpace(string(creationOutput))
if t, err := time.Parse("Mon Jan 2 15:04:05 2006", creationStr); err == nil {
@@ -900,7 +933,7 @@ func (s *ZFSService) CreateDataset(ctx context.Context, poolName string, req Cre
if err != nil {
s.logger.Error("Failed to save dataset to database", "name", fullName, "error", err)
// Try to destroy the dataset if we can't save to database
exec.CommandContext(ctx, "zfs", "destroy", "-r", fullName).Run()
zfsCommand(ctx, "destroy", "-r", fullName).Run()
return nil, fmt.Errorf("failed to save dataset to database: %w", err)
}
@@ -928,7 +961,7 @@ func (s *ZFSService) CreateDataset(ctx context.Context, poolName string, req Cre
func (s *ZFSService) DeleteDataset(ctx context.Context, datasetName string) error {
// Check if dataset exists and get its mount point before deletion
var mountPoint string
cmd := exec.CommandContext(ctx, "zfs", "list", "-H", "-o", "name,mountpoint", datasetName)
cmd := zfsCommand(ctx, "list", "-H", "-o", "name,mountpoint", datasetName)
output, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("dataset %s does not exist: %w", datasetName, err)
@@ -947,7 +980,7 @@ func (s *ZFSService) DeleteDataset(ctx context.Context, datasetName string) erro
// Get dataset type to determine if we should clean up mount directory
var datasetType string
typeCmd := exec.CommandContext(ctx, "zfs", "get", "-H", "-o", "value", "type", datasetName)
typeCmd := zfsCommand(ctx, "get", "-H", "-o", "value", "type", datasetName)
typeOutput, err := typeCmd.Output()
if err == nil {
datasetType = strings.TrimSpace(string(typeOutput))
@@ -970,7 +1003,7 @@ func (s *ZFSService) DeleteDataset(ctx context.Context, datasetName string) erro
// Delete the dataset from ZFS (use -r for recursive to delete children)
s.logger.Info("Deleting ZFS dataset", "name", datasetName, "mountpoint", mountPoint)
cmd = exec.CommandContext(ctx, "zfs", "destroy", "-r", datasetName)
cmd = zfsCommand(ctx, "destroy", "-r", datasetName)
output, err = cmd.CombinedOutput()
if err != nil {
errorMsg := string(output)