add some changes

This commit is contained in:
2026-01-15 09:44:57 +00:00
parent 9b1f85479b
commit 1d9406c93a
19 changed files with 4922 additions and 887 deletions

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