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) }