add some changes
This commit is contained in:
259
backend/internal/storage/snapshot.go
Normal file
259
backend/internal/storage/snapshot.go
Normal file
@@ -0,0 +1,259 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user