Files
storage-appliance/internal/service/storage/storage.go
2025-12-13 15:31:52 +00:00

103 lines
3.5 KiB
Go

package storage
import (
"context"
"errors"
"fmt"
"strings"
"github.com/example/storage-appliance/internal/audit"
"github.com/example/storage-appliance/internal/domain"
"github.com/example/storage-appliance/internal/infra/zfs"
"github.com/example/storage-appliance/internal/service"
)
var (
ErrForbidden = errors.New("forbidden")
ErrDuplicatePool = errors.New("duplicate pool name")
)
type StorageService struct {
ZFS zfs.Adapter
JobRunner service.JobRunner
Audit audit.AuditLogger
}
func NewStorageService(z *zfs.Adapter, jr service.JobRunner, al audit.AuditLogger) *StorageService {
return &StorageService{ZFS: *z, JobRunner: jr, Audit: al}
}
// ListPools returns pools via zfs adapter
func (s *StorageService) ListPools(ctx context.Context) ([]domain.Pool, error) {
return s.ZFS.ListPools(ctx)
}
// CreatePool validates and enqueues a create pool job; user must be admin
func (s *StorageService) CreatePool(ctx context.Context, user string, role string, name string, vdevs []string) (string, error) {
if role != "admin" {
return "", ErrForbidden
}
// Simple validation: new name not in existing pools
pools, err := s.ZFS.ListPools(ctx)
if err != nil {
return "", err
}
for _, p := range pools {
if p.Name == name {
return "", ErrDuplicatePool
}
}
// Create a job to build a pool. For skeleton, we just create a job entry with type create-pool
j := domain.Job{Type: "create-pool", Status: "queued", Owner: domain.UUID(user)}
id, err := s.JobRunner.Enqueue(ctx, j)
// Store details in audit
if s.Audit != nil {
s.Audit.Record(ctx, audit.Event{UserID: user, Action: "pool.create.request", ResourceType: "pool", ResourceID: name, Success: err == nil, Details: map[string]any{"vdevs": vdevs}})
}
return id, err
}
// Snapshot dataset
func (s *StorageService) Snapshot(ctx context.Context, user, role, dataset, snapName string) (string, error) {
if role != "admin" && role != "operator" {
return "", ErrForbidden
}
// call zfs snapshot, but do as job; enqueue
j := domain.Job{Type: "snapshot", Status: "queued", Owner: domain.UUID(user)}
id, err := s.JobRunner.Enqueue(ctx, j)
if s.Audit != nil {
s.Audit.Record(ctx, audit.Event{UserID: user, Action: "dataset.snapshot.request", ResourceType: "snapshot", ResourceID: fmt.Sprintf("%s@%s", dataset, snapName), Success: err == nil, Details: map[string]any{"dataset": dataset}})
}
return id, err
}
func (s *StorageService) ScrubStart(ctx context.Context, user, role, pool string) (string, error) {
if role != "admin" && role != "operator" {
return "", ErrForbidden
}
j := domain.Job{Type: "scrub", Status: "queued", Owner: domain.UUID(user)}
id, err := s.JobRunner.Enqueue(ctx, j)
if s.Audit != nil {
s.Audit.Record(ctx, audit.Event{UserID: user, Action: "pool.scrub.request", ResourceType: "pool", ResourceID: pool, Success: err == nil})
}
return id, err
}
// ListDatasets returns datasets for a pool
func (s *StorageService) ListDatasets(ctx context.Context, pool string) ([]domain.Dataset, error) {
return s.ZFS.ListDatasets(ctx, pool)
}
// CreateDataset creates dataset synchronously or as job; for skeleton, do sync
func (s *StorageService) CreateDataset(ctx context.Context, user, role, name string, props map[string]string) error {
if role != "admin" && role != "operator" {
return ErrForbidden
}
return s.ZFS.CreateDataset(ctx, name, props)
}
// GetPoolStatus calls the adapter
func (s *StorageService) GetPoolStatus(ctx context.Context, pool string) (domain.PoolHealth, error) {
return s.ZFS.GetPoolStatus(ctx, pool)
}