This commit is contained in:
@@ -255,6 +255,197 @@ func (s *Service) GetPool(name string) (*models.Pool, error) {
|
||||
return nil, fmt.Errorf("pool %s not found", name)
|
||||
}
|
||||
|
||||
// GetPoolDetail returns detailed pool information from zpool status
|
||||
func (s *Service) GetPoolDetail(name string) (*models.PoolDetail, error) {
|
||||
output, err := s.execCommand(s.zpoolPath, "status", name)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get pool status: %w", err)
|
||||
}
|
||||
|
||||
detail := &models.PoolDetail{
|
||||
Name: name,
|
||||
VDEVs: []models.VDEV{},
|
||||
Spares: []string{},
|
||||
}
|
||||
|
||||
lines := strings.Split(output, "\n")
|
||||
var currentVDEV *models.VDEV
|
||||
inConfig := false
|
||||
inSpares := false
|
||||
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
|
||||
// Parse pool name and state
|
||||
if strings.HasPrefix(line, "pool:") {
|
||||
parts := strings.Fields(line)
|
||||
if len(parts) > 1 {
|
||||
detail.Name = parts[1]
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.HasPrefix(line, "state:") {
|
||||
parts := strings.Fields(line)
|
||||
if len(parts) > 1 {
|
||||
detail.State = parts[1]
|
||||
detail.Status = strings.Join(parts[1:], " ")
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Parse errors line
|
||||
if strings.HasPrefix(line, "errors:") {
|
||||
detail.Errors = strings.TrimPrefix(line, "errors:")
|
||||
detail.Errors = strings.TrimSpace(detail.Errors)
|
||||
continue
|
||||
}
|
||||
|
||||
// Parse scrub information
|
||||
if strings.Contains(line, "scrub") {
|
||||
detail.ScrubInfo = line
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if we're entering config section
|
||||
if strings.HasPrefix(line, "config:") {
|
||||
inConfig = true
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if we're in spares section
|
||||
if strings.Contains(line, "spares") {
|
||||
inSpares = true
|
||||
inConfig = false
|
||||
continue
|
||||
}
|
||||
|
||||
// Parse VDEV and disk information
|
||||
if inConfig {
|
||||
// Check if this is a VDEV header (indented but not a disk)
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) >= 5 {
|
||||
// Check if it's a VDEV type line (mirror, raidz, etc.)
|
||||
if fields[0] == "mirror" || strings.HasPrefix(fields[0], "raidz") || fields[0] == "log" || fields[0] == "cache" {
|
||||
// Save previous VDEV if exists
|
||||
if currentVDEV != nil {
|
||||
detail.VDEVs = append(detail.VDEVs, *currentVDEV)
|
||||
}
|
||||
// Start new VDEV
|
||||
currentVDEV = &models.VDEV{
|
||||
Name: fields[0],
|
||||
Type: fields[0],
|
||||
State: "ONLINE",
|
||||
Disks: []models.Disk{},
|
||||
}
|
||||
// Try to parse state if available
|
||||
if len(fields) > 1 {
|
||||
for _, field := range fields[1:] {
|
||||
if field == "ONLINE" || field == "DEGRADED" || field == "FAULTED" || field == "OFFLINE" {
|
||||
currentVDEV.State = field
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if it's a disk line (starts with sd, hd, nvme, etc.)
|
||||
diskName := fields[0]
|
||||
if strings.HasPrefix(diskName, "sd") || strings.HasPrefix(diskName, "hd") || strings.HasPrefix(diskName, "nvme") {
|
||||
// This is a disk
|
||||
state := "ONLINE"
|
||||
read := 0
|
||||
write := 0
|
||||
checksum := 0
|
||||
|
||||
if len(fields) > 1 {
|
||||
state = fields[1]
|
||||
}
|
||||
if len(fields) > 2 {
|
||||
if val, err := strconv.Atoi(fields[2]); err == nil {
|
||||
read = val
|
||||
}
|
||||
}
|
||||
if len(fields) > 3 {
|
||||
if val, err := strconv.Atoi(fields[3]); err == nil {
|
||||
write = val
|
||||
}
|
||||
}
|
||||
if len(fields) > 4 {
|
||||
if val, err := strconv.Atoi(fields[4]); err == nil {
|
||||
checksum = val
|
||||
}
|
||||
}
|
||||
|
||||
disk := models.Disk{
|
||||
Name: diskName,
|
||||
State: state,
|
||||
Read: read,
|
||||
Write: write,
|
||||
Checksum: checksum,
|
||||
}
|
||||
|
||||
// If we have a current VDEV, add disk to it
|
||||
if currentVDEV != nil {
|
||||
currentVDEV.Disks = append(currentVDEV.Disks, disk)
|
||||
// Update VDEV errors
|
||||
currentVDEV.Read += read
|
||||
currentVDEV.Write += write
|
||||
currentVDEV.Checksum += checksum
|
||||
} else {
|
||||
// Standalone disk, create a VDEV for it
|
||||
currentVDEV = &models.VDEV{
|
||||
Name: diskName,
|
||||
Type: "disk",
|
||||
State: state,
|
||||
Disks: []models.Disk{disk},
|
||||
Read: read,
|
||||
Write: write,
|
||||
Checksum: checksum,
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Parse spares section
|
||||
if inSpares {
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) > 0 {
|
||||
diskName := fields[0]
|
||||
if strings.HasPrefix(diskName, "sd") || strings.HasPrefix(diskName, "hd") || strings.HasPrefix(diskName, "nvme") {
|
||||
detail.Spares = append(detail.Spares, diskName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Empty line might indicate end of section
|
||||
if line == "" && currentVDEV != nil {
|
||||
detail.VDEVs = append(detail.VDEVs, *currentVDEV)
|
||||
currentVDEV = nil
|
||||
}
|
||||
}
|
||||
|
||||
// Save last VDEV if exists
|
||||
if currentVDEV != nil {
|
||||
detail.VDEVs = append(detail.VDEVs, *currentVDEV)
|
||||
}
|
||||
|
||||
return detail, nil
|
||||
}
|
||||
|
||||
// AddSpareDisk adds a spare disk to a pool
|
||||
func (s *Service) AddSpareDisk(poolName, diskPath string) error {
|
||||
args := []string{"add", poolName, "spare", diskPath}
|
||||
_, err := s.execCommand(s.zpoolPath, args...)
|
||||
if err != nil {
|
||||
return translateZFSError(err, "menambahkan spare disk", poolName)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreatePool creates a new ZFS pool
|
||||
func (s *Service) CreatePool(name string, vdevs []string, options map[string]string) error {
|
||||
args := []string{"create"}
|
||||
@@ -1148,11 +1339,19 @@ func (s *Service) ListDisks() ([]map[string]string, error) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if disk is used in a pool
|
||||
isUsed := usedDisks[dev.Name]
|
||||
// Skip virtual disks (ZVOLs) - these are zd* devices
|
||||
// Must check BEFORE checking dev.Type == "disk" because zd* devices
|
||||
// are reported as type "disk" by lsblk
|
||||
if strings.HasPrefix(dev.Name, "zd") {
|
||||
log.Printf("debug: skipping virtual disk %s (zd* device)", dev.Name)
|
||||
continue
|
||||
}
|
||||
|
||||
// Include all disks (both available and used) so we can show status
|
||||
// Include all physical disks (both available and used) so we can show status
|
||||
// Only include actual disk devices (not partitions, loops, etc.)
|
||||
if dev.Type == "disk" {
|
||||
// Check if disk is used in a pool
|
||||
isUsed := usedDisks[dev.Name]
|
||||
disk := map[string]string{
|
||||
"name": dev.Name,
|
||||
"size": dev.Size,
|
||||
|
||||
Reference in New Issue
Block a user