This commit is contained in:
400
internal/zfs/service.go
Normal file
400
internal/zfs/service.go
Normal file
@@ -0,0 +1,400 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user