260 lines
7.1 KiB
Go
260 lines
7.1 KiB
Go
package storage
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/atlasos/calypso/internal/common/database"
|
|
"github.com/atlasos/calypso/internal/common/logger"
|
|
)
|
|
|
|
// SnapshotService handles ZFS snapshot operations
|
|
type SnapshotService struct {
|
|
db *database.DB
|
|
logger *logger.Logger
|
|
}
|
|
|
|
// NewSnapshotService creates a new snapshot service
|
|
func NewSnapshotService(db *database.DB, log *logger.Logger) *SnapshotService {
|
|
return &SnapshotService{
|
|
db: db,
|
|
logger: log,
|
|
}
|
|
}
|
|
|
|
// Snapshot represents a ZFS snapshot
|
|
type Snapshot struct {
|
|
ID string `json:"id"`
|
|
Name string `json:"name"` // Full snapshot name (e.g., "pool/dataset@snapshot-name")
|
|
Dataset string `json:"dataset"` // Dataset name (e.g., "pool/dataset")
|
|
SnapshotName string `json:"snapshot_name"` // Snapshot name only (e.g., "snapshot-name")
|
|
Created time.Time `json:"created"`
|
|
Referenced int64 `json:"referenced"` // Size in bytes
|
|
Used int64 `json:"used"` // Used space in bytes
|
|
IsLatest bool `json:"is_latest"` // Whether this is the latest snapshot for the dataset
|
|
}
|
|
|
|
// ListSnapshots lists all snapshots, optionally filtered by dataset
|
|
func (s *SnapshotService) ListSnapshots(ctx context.Context, datasetFilter string) ([]*Snapshot, error) {
|
|
// Build zfs list command
|
|
args := []string{"list", "-t", "snapshot", "-H", "-o", "name,creation,referenced,used"}
|
|
if datasetFilter != "" {
|
|
// List snapshots for specific dataset
|
|
args = append(args, datasetFilter)
|
|
} else {
|
|
// List all snapshots
|
|
args = append(args, "-r")
|
|
}
|
|
|
|
cmd := zfsCommand(ctx, args...)
|
|
output, err := cmd.Output()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to list snapshots: %w", err)
|
|
}
|
|
|
|
lines := strings.Split(strings.TrimSpace(string(output)), "\n")
|
|
var snapshots []*Snapshot
|
|
|
|
// Track latest snapshot per dataset
|
|
latestSnapshots := make(map[string]*Snapshot)
|
|
|
|
for _, line := range lines {
|
|
if line == "" {
|
|
continue
|
|
}
|
|
|
|
fields := strings.Fields(line)
|
|
if len(fields) < 4 {
|
|
continue
|
|
}
|
|
|
|
fullName := fields[0]
|
|
creationStr := fields[1]
|
|
referencedStr := fields[2]
|
|
usedStr := fields[3]
|
|
|
|
// Parse snapshot name (format: pool/dataset@snapshot-name)
|
|
parts := strings.Split(fullName, "@")
|
|
if len(parts) != 2 {
|
|
continue
|
|
}
|
|
|
|
dataset := parts[0]
|
|
snapshotName := parts[1]
|
|
|
|
// Parse creation time (Unix timestamp)
|
|
var creationTime time.Time
|
|
if timestamp, err := parseZFSUnixTimestamp(creationStr); err == nil {
|
|
creationTime = timestamp
|
|
} else {
|
|
// Fallback to current time if parsing fails
|
|
creationTime = time.Now()
|
|
}
|
|
|
|
// Parse sizes
|
|
referenced := parseSnapshotSize(referencedStr)
|
|
used := parseSnapshotSize(usedStr)
|
|
|
|
snapshot := &Snapshot{
|
|
ID: fullName,
|
|
Name: fullName,
|
|
Dataset: dataset,
|
|
SnapshotName: snapshotName,
|
|
Created: creationTime,
|
|
Referenced: referenced,
|
|
Used: used,
|
|
IsLatest: false,
|
|
}
|
|
|
|
snapshots = append(snapshots, snapshot)
|
|
|
|
// Track latest snapshot per dataset
|
|
if latest, exists := latestSnapshots[dataset]; !exists || snapshot.Created.After(latest.Created) {
|
|
latestSnapshots[dataset] = snapshot
|
|
}
|
|
}
|
|
|
|
// Mark latest snapshots
|
|
for _, snapshot := range snapshots {
|
|
if latest, exists := latestSnapshots[snapshot.Dataset]; exists && latest.ID == snapshot.ID {
|
|
snapshot.IsLatest = true
|
|
}
|
|
}
|
|
|
|
return snapshots, nil
|
|
}
|
|
|
|
// CreateSnapshot creates a new snapshot
|
|
func (s *SnapshotService) CreateSnapshot(ctx context.Context, dataset, snapshotName string, recursive bool) error {
|
|
// Validate dataset exists
|
|
cmd := zfsCommand(ctx, "list", "-H", "-o", "name", dataset)
|
|
if err := cmd.Run(); err != nil {
|
|
return fmt.Errorf("dataset %s does not exist: %w", dataset, err)
|
|
}
|
|
|
|
// Build snapshot name
|
|
fullSnapshotName := fmt.Sprintf("%s@%s", dataset, snapshotName)
|
|
|
|
// Build command
|
|
args := []string{"snapshot"}
|
|
if recursive {
|
|
args = append(args, "-r")
|
|
}
|
|
args = append(args, fullSnapshotName)
|
|
|
|
cmd = zfsCommand(ctx, args...)
|
|
output, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create snapshot: %s: %w", string(output), err)
|
|
}
|
|
|
|
s.logger.Info("Snapshot created", "snapshot", fullSnapshotName, "dataset", dataset, "recursive", recursive)
|
|
return nil
|
|
}
|
|
|
|
// DeleteSnapshot deletes a snapshot
|
|
func (s *SnapshotService) DeleteSnapshot(ctx context.Context, snapshotName string, recursive bool) error {
|
|
// Build command
|
|
args := []string{"destroy"}
|
|
if recursive {
|
|
args = append(args, "-r")
|
|
}
|
|
args = append(args, snapshotName)
|
|
|
|
cmd := zfsCommand(ctx, args...)
|
|
output, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to delete snapshot: %s: %w", string(output), err)
|
|
}
|
|
|
|
s.logger.Info("Snapshot deleted", "snapshot", snapshotName, "recursive", recursive)
|
|
return nil
|
|
}
|
|
|
|
// RollbackSnapshot rolls back a dataset to a snapshot
|
|
func (s *SnapshotService) RollbackSnapshot(ctx context.Context, snapshotName string, force bool) error {
|
|
// Build command
|
|
args := []string{"rollback"}
|
|
if force {
|
|
args = append(args, "-r", "-f")
|
|
} else {
|
|
args = append(args, "-r")
|
|
}
|
|
args = append(args, snapshotName)
|
|
|
|
cmd := zfsCommand(ctx, args...)
|
|
output, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to rollback snapshot: %s: %w", string(output), err)
|
|
}
|
|
|
|
s.logger.Info("Snapshot rollback completed", "snapshot", snapshotName, "force", force)
|
|
return nil
|
|
}
|
|
|
|
// CloneSnapshot clones a snapshot to a new dataset
|
|
func (s *SnapshotService) CloneSnapshot(ctx context.Context, snapshotName, cloneName string) error {
|
|
// Build command
|
|
args := []string{"clone", snapshotName, cloneName}
|
|
|
|
cmd := zfsCommand(ctx, args...)
|
|
output, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to clone snapshot: %s: %w", string(output), err)
|
|
}
|
|
|
|
s.logger.Info("Snapshot cloned", "snapshot", snapshotName, "clone", cloneName)
|
|
return nil
|
|
}
|
|
|
|
// parseZFSUnixTimestamp parses ZFS Unix timestamp string
|
|
func parseZFSUnixTimestamp(timestampStr string) (time.Time, error) {
|
|
// ZFS returns Unix timestamp as string
|
|
timestamp, err := time.Parse("20060102150405", timestampStr)
|
|
if err != nil {
|
|
// Try parsing as Unix timestamp integer
|
|
var unixTime int64
|
|
if _, err := fmt.Sscanf(timestampStr, "%d", &unixTime); err == nil {
|
|
return time.Unix(unixTime, 0), nil
|
|
}
|
|
return time.Time{}, err
|
|
}
|
|
return timestamp, nil
|
|
}
|
|
|
|
// parseZFSSize parses ZFS size string (e.g., "1.2M", "4.5G", "850K")
|
|
// Note: This function is also defined in zfs.go, but we need it here for snapshot parsing
|
|
func parseSnapshotSize(sizeStr string) int64 {
|
|
if sizeStr == "-" || sizeStr == "" {
|
|
return 0
|
|
}
|
|
|
|
// Remove any whitespace
|
|
sizeStr = strings.TrimSpace(sizeStr)
|
|
|
|
// Parse size with unit
|
|
var size float64
|
|
var unit string
|
|
if _, err := fmt.Sscanf(sizeStr, "%f%s", &size, &unit); err != nil {
|
|
return 0
|
|
}
|
|
|
|
// Convert to bytes
|
|
unit = strings.ToUpper(unit)
|
|
switch unit {
|
|
case "K", "KB":
|
|
return int64(size * 1024)
|
|
case "M", "MB":
|
|
return int64(size * 1024 * 1024)
|
|
case "G", "GB":
|
|
return int64(size * 1024 * 1024 * 1024)
|
|
case "T", "TB":
|
|
return int64(size * 1024 * 1024 * 1024 * 1024)
|
|
default:
|
|
// Assume bytes if no unit
|
|
return int64(size)
|
|
}
|
|
}
|