package zfs import ( "bytes" "encoding/json" "fmt" "os/exec" "strconv" "strings" "time" "gitea.avt.data-center.id/othman.suseno/atlas/internal/models" ) // Service provides ZFS operations type Service struct { zfsPath string zpoolPath string } // New creates a new ZFS service func New() *Service { return &Service{ zfsPath: "zfs", zpoolPath: "zpool", } } // execCommand executes a shell command and returns output func (s *Service) execCommand(name string, args ...string) (string, error) { cmd := exec.Command(name, args...) var stdout, stderr bytes.Buffer cmd.Stdout = &stdout cmd.Stderr = &stderr if err := cmd.Run(); err != nil { return "", fmt.Errorf("%s: %v: %s", name, err, stderr.String()) } return strings.TrimSpace(stdout.String()), nil } // ListPools returns all ZFS pools func (s *Service) ListPools() ([]models.Pool, error) { output, err := s.execCommand(s.zpoolPath, "list", "-H", "-o", "name,size,allocated,free,health") if err != nil { return nil, err } var pools []models.Pool lines := strings.Split(output, "\n") for _, line := range lines { if line == "" { continue } fields := strings.Fields(line) if len(fields) < 5 { continue } pool := models.Pool{ Name: fields[0], Status: "ONLINE", // Default, will be updated from health Health: fields[4], } // Parse sizes (handles K, M, G, T suffixes) if size, err := parseSize(fields[1]); err == nil { pool.Size = size } if allocated, err := parseSize(fields[2]); err == nil { pool.Allocated = allocated } if free, err := parseSize(fields[3]); err == nil { pool.Free = free } // Get pool status status, _ := s.execCommand(s.zpoolPath, "status", "-x", pool.Name) if strings.Contains(status, "all pools are healthy") { pool.Status = "ONLINE" } else if strings.Contains(status, "DEGRADED") { pool.Status = "DEGRADED" } else if strings.Contains(status, "FAULTED") { pool.Status = "FAULTED" } // Get creation time created, _ := s.execCommand(s.zfsPath, "get", "-H", "-o", "value", "creation", pool.Name) if t, err := time.Parse("Mon Jan 2 15:04:05 2006", created); err == nil { pool.CreatedAt = t } pools = append(pools, pool) } return pools, nil } // GetPool returns a specific pool func (s *Service) GetPool(name string) (*models.Pool, error) { pools, err := s.ListPools() if err != nil { return nil, err } for _, pool := range pools { if pool.Name == name { return &pool, nil } } return nil, fmt.Errorf("pool %s not found", name) } // CreatePool creates a new ZFS pool func (s *Service) CreatePool(name string, vdevs []string, options map[string]string) error { args := []string{"create"} // Add options for k, v := range options { args = append(args, "-o", fmt.Sprintf("%s=%s", k, v)) } args = append(args, name) args = append(args, vdevs...) _, err := s.execCommand(s.zpoolPath, args...) return err } // DestroyPool destroys a ZFS pool func (s *Service) DestroyPool(name string) error { _, err := s.execCommand(s.zpoolPath, "destroy", name) return err } // ScrubPool starts a scrub operation on a pool func (s *Service) ScrubPool(name string) error { _, err := s.execCommand(s.zpoolPath, "scrub", name) return err } // GetScrubStatus returns the current scrub status func (s *Service) GetScrubStatus(name string) (string, error) { output, err := s.execCommand(s.zpoolPath, "status", name) if err != nil { return "", err } if strings.Contains(output, "scrub in progress") { return "in_progress", nil } if strings.Contains(output, "scrub repaired") { return "completed", nil } return "idle", nil } // ListDatasets returns all datasets in a pool (or all if pool is empty) func (s *Service) ListDatasets(pool string) ([]models.Dataset, error) { args := []string{"list", "-H", "-o", "name,type,used,avail,mountpoint"} if pool != "" { args = append(args, "-r", pool) } else { args = append(args, "-r") } output, err := s.execCommand(s.zfsPath, args...) if err != nil { return nil, err } var datasets []models.Dataset lines := strings.Split(output, "\n") for _, line := range lines { if line == "" { continue } fields := strings.Fields(line) if len(fields) < 5 { continue } fullName := fields[0] parts := strings.Split(fullName, "/") poolName := parts[0] dataset := models.Dataset{ Name: fullName, Pool: poolName, Type: fields[1], Mountpoint: fields[4], } if used, err := parseSize(fields[2]); err == nil { dataset.Used = used } if avail, err := parseSize(fields[3]); err == nil { dataset.Available = avail } dataset.Size = dataset.Used + dataset.Available // Get creation time created, _ := s.execCommand(s.zfsPath, "get", "-H", "-o", "value", "creation", fullName) if t, err := time.Parse("Mon Jan 2 15:04:05 2006", created); err == nil { dataset.CreatedAt = t } datasets = append(datasets, dataset) } return datasets, nil } // CreateDataset creates a new ZFS dataset func (s *Service) CreateDataset(name string, options map[string]string) error { args := []string{"create"} for k, v := range options { args = append(args, "-o", fmt.Sprintf("%s=%s", k, v)) } args = append(args, name) _, err := s.execCommand(s.zfsPath, args...) return err } // DestroyDataset destroys a ZFS dataset func (s *Service) DestroyDataset(name string, recursive bool) error { args := []string{"destroy"} if recursive { args = append(args, "-r") } args = append(args, name) _, err := s.execCommand(s.zfsPath, args...) return err } // ListZVOLs returns all ZVOLs func (s *Service) ListZVOLs(pool string) ([]models.ZVOL, error) { args := []string{"list", "-H", "-o", "name,volsize,used", "-t", "volume"} if pool != "" { args = append(args, "-r", pool) } else { args = append(args, "-r") } output, err := s.execCommand(s.zfsPath, args...) if err != nil { return nil, err } var zvols []models.ZVOL lines := strings.Split(output, "\n") for _, line := range lines { if line == "" { continue } fields := strings.Fields(line) if len(fields) < 3 { continue } fullName := fields[0] parts := strings.Split(fullName, "/") poolName := parts[0] zvol := models.ZVOL{ Name: fullName, Pool: poolName, } if size, err := parseSize(fields[1]); err == nil { zvol.Size = size } if used, err := parseSize(fields[2]); err == nil { zvol.Used = used } // Get creation time created, _ := s.execCommand(s.zfsPath, "get", "-H", "-o", "value", "creation", fullName) if t, err := time.Parse("Mon Jan 2 15:04:05 2006", created); err == nil { zvol.CreatedAt = t } zvols = append(zvols, zvol) } return zvols, nil } // CreateZVOL creates a new ZVOL func (s *Service) CreateZVOL(name string, size uint64, options map[string]string) error { args := []string{"create", "-V", fmt.Sprintf("%d", size)} for k, v := range options { args = append(args, "-o", fmt.Sprintf("%s=%s", k, v)) } args = append(args, name) _, err := s.execCommand(s.zfsPath, args...) return err } // DestroyZVOL destroys a ZVOL func (s *Service) DestroyZVOL(name string) error { _, err := s.execCommand(s.zfsPath, "destroy", name) return err } // ListDisks returns available disks (read-only) func (s *Service) ListDisks() ([]map[string]string, error) { // Use lsblk to list block devices output, err := s.execCommand("lsblk", "-J", "-o", "name,size,type,fstype,mountpoint") if err != nil { return nil, err } var result struct { BlockDevices []struct { Name string `json:"name"` Size string `json:"size"` Type string `json:"type"` FSType string `json:"fstype"` Mountpoint string `json:"mountpoint"` Children []interface{} `json:"children"` } `json:"blockdevices"` } if err := json.Unmarshal([]byte(output), &result); err != nil { return nil, err } var disks []map[string]string for _, dev := range result.BlockDevices { if dev.Type == "disk" && dev.FSType == "" && dev.Mountpoint == "" { disks = append(disks, map[string]string{ "name": dev.Name, "size": dev.Size, "path": "/dev/" + dev.Name, }) } } return disks, nil } // parseSize converts human-readable size to bytes func parseSize(s string) (uint64, error) { s = strings.TrimSpace(s) if s == "-" || s == "" { return 0, nil } multiplier := uint64(1) suffix := strings.ToUpper(s[len(s)-1:]) switch suffix { case "K": multiplier = 1024 s = s[:len(s)-1] case "M": multiplier = 1024 * 1024 s = s[:len(s)-1] case "G": multiplier = 1024 * 1024 * 1024 s = s[:len(s)-1] case "T": multiplier = 1024 * 1024 * 1024 * 1024 s = s[:len(s)-1] case "P": multiplier = 1024 * 1024 * 1024 * 1024 * 1024 s = s[:len(s)-1] default: // Check if last char is a digit if suffix[0] < '0' || suffix[0] > '9' { return 0, fmt.Errorf("unknown suffix: %s", suffix) } } // Handle decimal values (e.g., "1.5G") if strings.Contains(s, ".") { val, err := strconv.ParseFloat(s, 64) if err != nil { return 0, err } return uint64(val * float64(multiplier)), nil } val, err := strconv.ParseUint(s, 10, 64) if err != nil { return 0, err } return val * multiplier, nil }