package services import ( "fmt" "os/exec" "strconv" "strings" "sync" "gitea.avt.data-center.id/othman.suseno/atlas/internal/models" ) // ISCSIService manages iSCSI target service integration type ISCSIService struct { mu sync.RWMutex targetcliPath string } // NewISCSIService creates a new iSCSI service manager func NewISCSIService() *ISCSIService { // Try targetcli first, fallback to targetcli-fb targetcliPath := "targetcli" if _, err := exec.LookPath("targetcli"); err != nil { if _, err := exec.LookPath("targetcli-fb"); err == nil { targetcliPath = "targetcli-fb" } } return &ISCSIService{ targetcliPath: targetcliPath, } } // ApplyConfiguration applies iSCSI target configurations func (s *ISCSIService) ApplyConfiguration(targets []models.ISCSITarget) error { s.mu.Lock() defer s.mu.Unlock() // For each target, ensure it exists and is configured for _, target := range targets { if !target.Enabled { // Disable target if it exists if err := s.disableTarget(target.IQN); err != nil { // Log but continue fmt.Printf("warning: failed to disable target %s: %v\n", target.IQN, err) } continue } // Create or update target if err := s.createTarget(target); err != nil { return fmt.Errorf("create target %s: %w", target.IQN, err) } fmt.Printf("iSCSI target created/verified: %s\n", target.IQN) // Configure ACLs if err := s.configureACLs(target); err != nil { return fmt.Errorf("configure ACLs for %s: %w", target.IQN, err) } fmt.Printf("iSCSI ACLs configured for: %s\n", target.IQN) // Configure LUNs if err := s.configureLUNs(target); err != nil { return fmt.Errorf("configure LUNs for %s: %w", target.IQN, err) } fmt.Printf("iSCSI LUNs configured for: %s\n", target.IQN) } return nil } // createTarget creates an iSCSI target func (s *ISCSIService) createTarget(target models.ISCSITarget) error { // Use targetcli to create target // Format: targetcli /iscsi create cmd := exec.Command("sudo", "-n", s.targetcliPath, "/iscsi", "create", target.IQN) output, err := cmd.CombinedOutput() if err != nil { // Target might already exist, which is OK // Check if it actually exists if !s.targetExists(target.IQN) { return fmt.Errorf("create target failed: %w (output: %s)", err, string(output)) } fmt.Printf("target %s already exists, continuing\n", target.IQN) } else { fmt.Printf("target %s created successfully\n", target.IQN) } // Enable TPG1 (Target Portal Group 1) // Disable authentication cmd = exec.Command("sudo", "-n", s.targetcliPath, "/iscsi/"+target.IQN+"/tpg1", "set", "attribute", "authentication=0") if output, err := cmd.CombinedOutput(); err != nil { fmt.Printf("warning: failed to set authentication=0: %v (output: %s)\n", err, string(output)) } // Enable generate_node_acls (allow all initiators if no ACLs specified) cmd = exec.Command("sudo", "-n", s.targetcliPath, "/iscsi/"+target.IQN+"/tpg1", "set", "attribute", "generate_node_acls=1") if output, err := cmd.CombinedOutput(); err != nil { fmt.Printf("warning: failed to set generate_node_acls=1: %v (output: %s)\n", err, string(output)) } else { fmt.Printf("set generate_node_acls=1 for target %s\n", target.IQN) } // Create portal if not exists (listen on all interfaces, port 3260) cmd = exec.Command("sudo", "-n", s.targetcliPath, "/iscsi/"+target.IQN+"/tpg1/portals", "create") if err := cmd.Run(); err != nil { // Portal might already exist, which is OK // Check if portal exists cmd = exec.Command("sudo", "-n", s.targetcliPath, "/iscsi/"+target.IQN+"/tpg1/portals", "ls") output, err2 := cmd.Output() if err2 != nil || len(strings.TrimSpace(string(output))) == 0 { // No portal exists, try to create with specific IP // Get system IP systemIP, _ := s.getSystemIP() cmd = exec.Command("sudo", "-n", s.targetcliPath, "/iscsi/"+target.IQN+"/tpg1/portals", "create", systemIP) if err3 := cmd.Run(); err3 != nil { // Try with 0.0.0.0 (all interfaces) cmd = exec.Command("sudo", "-n", s.targetcliPath, "/iscsi/"+target.IQN+"/tpg1/portals", "create", "0.0.0.0") if err4 := cmd.Run(); err4 != nil { // Log but don't fail - portal might already exist fmt.Printf("warning: failed to create portal: %v", err4) } } } } // Save configuration cmd = exec.Command("sudo", "-n", s.targetcliPath, "saveconfig") cmd.Run() // Ignore errors return nil } // configureACLs configures initiator ACLs for a target func (s *ISCSIService) configureACLs(target models.ISCSITarget) error { // If no initiators specified, allow all (generate_node_acls=1) if len(target.Initiators) == 0 { // Set to allow all initiators cmd := exec.Command("sudo", "-n", s.targetcliPath, "/iscsi/"+target.IQN+"/tpg1", "set", "attribute", "generate_node_acls=1") if err := cmd.Run(); err != nil { return fmt.Errorf("set generate_node_acls: %w", err) } // Disable authentication for open access cmd = exec.Command("sudo", "-n", s.targetcliPath, "/iscsi/"+target.IQN+"/tpg1", "set", "attribute", "authentication=0") cmd.Run() // Ignore errors return nil } // If initiators specified, use ACL-based access // Set generate_node_acls=0 to use explicit ACLs cmd := exec.Command("sudo", "-n", s.targetcliPath, "/iscsi/"+target.IQN+"/tpg1", "set", "attribute", "generate_node_acls=0") if err := cmd.Run(); err != nil { return fmt.Errorf("set generate_node_acls=0: %w", err) } // Get current ACLs currentACLs, _ := s.getACLs(target.IQN) // Remove ACLs not in desired list for _, acl := range currentACLs { if !contains(target.Initiators, acl) { cmd := exec.Command("sudo", "-n", s.targetcliPath, "/iscsi/"+target.IQN+"/tpg1/acls", "delete", acl) cmd.Run() // Ignore errors } } // Add new ACLs for _, initiator := range target.Initiators { if !contains(currentACLs, initiator) { cmd := exec.Command("sudo", "-n", s.targetcliPath, "/iscsi/"+target.IQN+"/tpg1/acls", "create", initiator) if err := cmd.Run(); err != nil { return fmt.Errorf("create ACL %s: %w", initiator, err) } } } return nil } // configureLUNs configures LUNs for a target func (s *ISCSIService) configureLUNs(target models.ISCSITarget) error { // Get current LUNs currentLUNs, _ := s.getLUNs(target.IQN) // Remove LUNs not in desired list for _, lun := range currentLUNs { if !s.hasLUN(target.LUNs, lun) { cmd := exec.Command("sudo", "-n", s.targetcliPath, "/iscsi/"+target.IQN+"/tpg1/luns", "delete", fmt.Sprintf("lun/%d", lun)) cmd.Run() // Ignore errors } } // Add/update LUNs for _, lun := range target.LUNs { // Determine backstore type (default to block for ZVOL, pscsi for tape devices) backstoreType := lun.Backstore if backstoreType == "" { if lun.Device != "" { // If device is specified and looks like tape device, use pscsi if strings.HasPrefix(lun.Device, "/dev/st") || strings.HasPrefix(lun.Device, "/dev/nst") { backstoreType = "pscsi" } else { backstoreType = "block" } } else if lun.ZVOL != "" { // Default to block for ZVOL backstoreType = "block" } else { return fmt.Errorf("LUN must have either ZVOL or Device specified") } } // Determine backstore name backstoreName := lun.BackstoreName if backstoreName == "" { if lun.ZVOL != "" { backstoreName = strings.ReplaceAll(lun.ZVOL, "/", "-") } else if lun.Device != "" { // Use device name (e.g., st0, sdb) backstoreName = strings.TrimPrefix(strings.TrimPrefix(lun.Device, "/dev/"), "/dev/") backstoreName = strings.ReplaceAll(backstoreName, "/", "-") } else { backstoreName = fmt.Sprintf("lun-%d", lun.ID) } } // Determine device path var devicePath string if lun.Device != "" { devicePath = lun.Device } else if lun.ZVOL != "" { devicePath = "/dev/zvol/" + lun.ZVOL } else { return fmt.Errorf("LUN must have either ZVOL or Device specified") } backstorePath := "/backstores/" + backstoreType + "/" + backstoreName // Create backstore based on type switch backstoreType { case "block": // Format: targetcli /backstores/block create name= dev= cmd := exec.Command("sudo", "-n", s.targetcliPath, "/backstores/block", "create", "name="+backstoreName, "dev="+devicePath) if output, err := cmd.CombinedOutput(); err != nil { if !strings.Contains(string(output), "already exists") { fmt.Printf("warning: failed to create block backstore %s: %v (output: %s)\n", backstoreName, err, string(output)) } } else { fmt.Printf("created block backstore %s for %s\n", backstoreName, devicePath) } case "pscsi": // Format: targetcli /backstores/pscsi create name= dev= // pscsi is for SCSI pass-through (tape devices, etc.) cmd := exec.Command("sudo", "-n", s.targetcliPath, "/backstores/pscsi", "create", "name="+backstoreName, "dev="+devicePath) if output, err := cmd.CombinedOutput(); err != nil { if !strings.Contains(string(output), "already exists") { return fmt.Errorf("failed to create pscsi backstore %s: %w (output: %s)", backstoreName, err, string(output)) } } else { fmt.Printf("created pscsi backstore %s for %s\n", backstoreName, devicePath) } case "fileio": // Format: targetcli /backstores/fileio create name= file_or_dev= [size=] // fileio is for file-based storage cmd := exec.Command("sudo", "-n", s.targetcliPath, "/backstores/fileio", "create", "name="+backstoreName, "file_or_dev="+devicePath) if output, err := cmd.CombinedOutput(); err != nil { if !strings.Contains(string(output), "already exists") { return fmt.Errorf("failed to create fileio backstore %s: %w (output: %s)", backstoreName, err, string(output)) } } else { fmt.Printf("created fileio backstore %s for %s\n", backstoreName, devicePath) } default: return fmt.Errorf("unsupported backstore type: %s", backstoreType) } // Create LUN cmd := exec.Command("sudo", "-n", s.targetcliPath, "/iscsi/"+target.IQN+"/tpg1/luns", "create", backstorePath) if err := cmd.Run(); err != nil { // LUN might already exist if !s.hasLUNID(currentLUNs, lun.ID) { return fmt.Errorf("create LUN %d: %w", lun.ID, err) } } } return nil } // Helper functions func (s *ISCSIService) targetExists(iqn string) bool { cmd := exec.Command("sudo", "-n", s.targetcliPath, "/iscsi", "ls") output, err := cmd.Output() if err != nil { return false } return strings.Contains(string(output), iqn) } func (s *ISCSIService) getACLs(iqn string) ([]string, error) { cmd := exec.Command("sudo", "-n", s.targetcliPath, "/iscsi/"+iqn+"/tpg1/acls", "ls") output, err := cmd.Output() if err != nil { return []string{}, nil // Return empty if can't get ACLs } // Parse output to extract ACL names // Format: o- acls ................................................................................................ [ACLs: 1] // o- iqn.1994-05.com.redhat:client1 ........................................................ [Mapped LUNs: 1] lines := strings.Split(string(output), "\n") acls := []string{} for _, line := range lines { line = strings.TrimSpace(line) if strings.HasPrefix(line, "o- iqn.") { // Extract IQN from line like "o- iqn.1994-05.com.redhat:client1" parts := strings.Fields(line) if len(parts) >= 2 && strings.HasPrefix(parts[1], "iqn.") { acls = append(acls, parts[1]) } } } return acls, nil } func (s *ISCSIService) getLUNs(iqn string) ([]int, error) { cmd := exec.Command("sudo", "-n", s.targetcliPath, "/iscsi/"+iqn+"/tpg1/luns", "ls") output, err := cmd.Output() if err != nil { return []int{}, nil // Return empty if can't get LUNs } // Parse output to extract LUN IDs // Format: o- luns ................................................................................................ [LUNs: 1] // o- lun0 ................................................................................ [zvol/pool-test-02/vol-1(/dev/zvol/pool-test-02/vol-1)] lines := strings.Split(string(output), "\n") luns := []int{} for _, line := range lines { line = strings.TrimSpace(line) if strings.HasPrefix(line, "o- lun") { // Extract LUN number from line like "o- lun0" parts := strings.Fields(line) if len(parts) >= 2 && strings.HasPrefix(parts[1], "lun") { lunIDStr := strings.TrimPrefix(parts[1], "lun") if lunID, err := strconv.Atoi(lunIDStr); err == nil { luns = append(luns, lunID) } } } } return luns, nil } func (s *ISCSIService) hasLUN(luns []models.LUN, id int) bool { for _, lun := range luns { if lun.ID == id { return true } } return false } func (s *ISCSIService) hasLUNID(luns []int, id int) bool { for _, lunID := range luns { if lunID == id { return true } } return false } func (s *ISCSIService) disableTarget(iqn string) error { cmd := exec.Command("sudo", "-n", s.targetcliPath, "/iscsi/"+iqn+"/tpg1", "set", "attribute", "enable=0") return cmd.Run() } // GetStatus returns the status of iSCSI target service func (s *ISCSIService) GetStatus() (bool, error) { // Check if targetd is running cmd := exec.Command("systemctl", "is-active", "target") if err := cmd.Run(); err == nil { return true, nil } // Fallback: check process cmd = exec.Command("pgrep", "-x", "targetd") if err := cmd.Run(); err == nil { return true, nil } return false, nil } func contains(slice []string, item string) bool { for _, s := range slice { if s == item { return true } } return false } // ConnectionInstructions represents iSCSI connection instructions type ConnectionInstructions struct { IQN string `json:"iqn"` Portal string `json:"portal"` // IP:port PortalIP string `json:"portal_ip"` // IP address PortalPort int `json:"portal_port"` // Port (default 3260) LUNs []LUNInfo `json:"luns"` Commands Commands `json:"commands"` } // LUNInfo represents LUN information for connection type LUNInfo struct { ID int `json:"id"` ZVOL string `json:"zvol"` Size uint64 `json:"size"` } // Commands contains OS-specific connection commands type Commands struct { Linux []string `json:"linux"` Windows []string `json:"windows"` MacOS []string `json:"macos"` } // GetConnectionInstructions generates connection instructions for an iSCSI target func (s *ISCSIService) GetConnectionInstructions(target models.ISCSITarget, portalIP string, portalPort int) *ConnectionInstructions { if portalPort == 0 { portalPort = 3260 // Default iSCSI port } portal := fmt.Sprintf("%s:%d", portalIP, portalPort) // Build LUN information luns := make([]LUNInfo, len(target.LUNs)) for i, lun := range target.LUNs { luns[i] = LUNInfo{ ID: lun.ID, ZVOL: lun.ZVOL, Size: lun.Size, } } // Generate Linux commands linuxCmds := []string{ fmt.Sprintf("# Discover target"), fmt.Sprintf("iscsiadm -m discovery -t sendtargets -p %s", portal), fmt.Sprintf(""), fmt.Sprintf("# Login to target"), fmt.Sprintf("iscsiadm -m node -T %s -p %s --login", target.IQN, portal), fmt.Sprintf(""), fmt.Sprintf("# Verify connection"), fmt.Sprintf("iscsiadm -m session"), fmt.Sprintf(""), fmt.Sprintf("# Logout (when done)"), fmt.Sprintf("iscsiadm -m node -T %s -p %s --logout", target.IQN, portal), } // Generate Windows commands windowsCmds := []string{ fmt.Sprintf("# Open PowerShell as Administrator"), fmt.Sprintf(""), fmt.Sprintf("# Add iSCSI target portal"), fmt.Sprintf("New-IscsiTargetPortal -TargetPortalAddress %s -TargetPortalPortNumber %d", portalIP, portalPort), fmt.Sprintf(""), fmt.Sprintf("# Connect to target"), fmt.Sprintf("Connect-IscsiTarget -NodeAddress %s", target.IQN), fmt.Sprintf(""), fmt.Sprintf("# Verify connection"), fmt.Sprintf("Get-IscsiSession"), fmt.Sprintf(""), fmt.Sprintf("# Disconnect (when done)"), fmt.Sprintf("Disconnect-IscsiTarget -NodeAddress %s", target.IQN), } // Generate macOS commands macosCmds := []string{ fmt.Sprintf("# macOS uses built-in iSCSI support"), fmt.Sprintf("# Use System Preferences > Network > iSCSI"), fmt.Sprintf(""), fmt.Sprintf("# Or use command line (if iscsiutil is available)"), fmt.Sprintf("iscsiutil -a -t %s -p %s", target.IQN, portal), fmt.Sprintf(""), fmt.Sprintf("# Portal: %s", portal), fmt.Sprintf("# Target IQN: %s", target.IQN), } return &ConnectionInstructions{ IQN: target.IQN, Portal: portal, PortalIP: portalIP, PortalPort: portalPort, LUNs: luns, Commands: Commands{ Linux: linuxCmds, Windows: windowsCmds, MacOS: macosCmds, }, } } // GetPortalIP attempts to detect the portal IP address func (s *ISCSIService) GetPortalIP() (string, error) { // Try to get IP from targetcli cmd := exec.Command("sudo", "-n", s.targetcliPath, "/iscsi", "ls") output, err := cmd.Output() if err != nil { // Fallback: try to get system IP return s.getSystemIP() } // Parse output to find portal IP // This is a simplified version - real implementation would parse targetcli output properly lines := strings.Split(string(output), "\n") for _, line := range lines { if strings.Contains(line, "ipv4") || strings.Contains(line, "ipv6") { // Extract IP from line parts := strings.Fields(line) for _, part := range parts { // Check if it looks like an IP address if strings.Contains(part, ".") || strings.Contains(part, ":") { // Remove port if present if idx := strings.Index(part, ":"); idx > 0 { return part[:idx], nil } return part, nil } } } } // Fallback to system IP return s.getSystemIP() } // getSystemIP gets a system IP address (simplified) func (s *ISCSIService) getSystemIP() (string, error) { // Try to get IP from hostname -I (Linux) cmd := exec.Command("hostname", "-I") output, err := cmd.Output() if err == nil { ips := strings.Fields(string(output)) if len(ips) > 0 { return ips[0], nil } } // Fallback: return localhost return "127.0.0.1", nil }