103 lines
3.5 KiB
Go
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)
|
|
}
|