package services import ( "fmt" "os/exec" "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 } continue } // Create or update target if err := s.createTarget(target); err != nil { return fmt.Errorf("create target %s: %w", target.IQN, err) } // Configure ACLs if err := s.configureACLs(target); err != nil { return fmt.Errorf("configure ACLs for %s: %w", target.IQN, err) } // Configure LUNs if err := s.configureLUNs(target); err != nil { return fmt.Errorf("configure LUNs for %s: %w", target.IQN, err) } } 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(s.targetcliPath, "/iscsi", "create", target.IQN) if err := cmd.Run(); 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", err) } } return nil } // configureACLs configures initiator ACLs for a target func (s *ISCSIService) configureACLs(target models.ISCSITarget) error { // 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(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(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(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 { // Create LUN mapping // Format: targetcli /iscsi//tpg1/luns create /backstores/zvol/ zvolPath := "/backstores/zvol/" + lun.ZVOL // First ensure the zvol backend exists cmd := exec.Command(s.targetcliPath, "/backstores/zvol", "create", lun.ZVOL, lun.ZVOL) cmd.Run() // Ignore if already exists // Create LUN cmd = exec.Command(s.targetcliPath, "/iscsi/"+target.IQN+"/tpg1/luns", "create", zvolPath) 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(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(s.targetcliPath, "/iscsi/"+iqn+"/tpg1/acls", "ls") _, err := cmd.Output() if err != nil { return nil, err } // Parse output to extract ACL names // This is simplified - real implementation would parse targetcli output return []string{}, nil } func (s *ISCSIService) getLUNs(iqn string) ([]int, error) { cmd := exec.Command(s.targetcliPath, "/iscsi/"+iqn+"/tpg1/luns", "ls") _, err := cmd.Output() if err != nil { return nil, err } // Parse output to extract LUN IDs // This is simplified - real implementation would parse targetcli output return []int{}, 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(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(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 }