fix storage
This commit is contained in:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user