fixing UI and iscsi sync
Some checks failed
CI / test-build (push) Has been cancelled

This commit is contained in:
2025-12-20 19:16:50 +00:00
parent 2bb892dfdc
commit 6202ef8e83
12 changed files with 1436 additions and 116 deletions

View File

@@ -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,