This commit is contained in:
@@ -3,6 +3,7 @@ package services
|
||||
import (
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
@@ -41,6 +42,7 @@ func (s *ISCSIService) ApplyConfiguration(targets []models.ISCSITarget) error {
|
||||
// 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
|
||||
}
|
||||
@@ -49,16 +51,19 @@ func (s *ISCSIService) ApplyConfiguration(targets []models.ISCSITarget) error {
|
||||
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
|
||||
@@ -68,27 +73,93 @@ func (s *ISCSIService) ApplyConfiguration(targets []models.ISCSITarget) error {
|
||||
func (s *ISCSIService) createTarget(target models.ISCSITarget) error {
|
||||
// Use targetcli to create target
|
||||
// Format: targetcli /iscsi create <IQN>
|
||||
cmd := exec.Command(s.targetcliPath, "/iscsi", "create", target.IQN)
|
||||
if err := cmd.Run(); err != nil {
|
||||
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", err)
|
||||
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(s.targetcliPath, "/iscsi/"+target.IQN+"/tpg1/acls", "delete", acl)
|
||||
cmd := exec.Command("sudo", "-n", s.targetcliPath, "/iscsi/"+target.IQN+"/tpg1/acls", "delete", acl)
|
||||
cmd.Run() // Ignore errors
|
||||
}
|
||||
}
|
||||
@@ -96,7 +167,7 @@ func (s *ISCSIService) configureACLs(target models.ISCSITarget) error {
|
||||
// Add new ACLs
|
||||
for _, initiator := range target.Initiators {
|
||||
if !contains(currentACLs, initiator) {
|
||||
cmd := exec.Command(s.targetcliPath, "/iscsi/"+target.IQN+"/tpg1/acls", "create", 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)
|
||||
}
|
||||
@@ -114,23 +185,100 @@ func (s *ISCSIService) configureLUNs(target models.ISCSITarget) error {
|
||||
// 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 := 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 {
|
||||
// Create LUN mapping
|
||||
// Format: targetcli /iscsi/<IQN>/tpg1/luns create /backstores/zvol/<zvol>
|
||||
zvolPath := "/backstores/zvol/" + lun.ZVOL
|
||||
// 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")
|
||||
}
|
||||
}
|
||||
|
||||
// First ensure the zvol backend exists
|
||||
cmd := exec.Command(s.targetcliPath, "/backstores/zvol", "create", lun.ZVOL, lun.ZVOL)
|
||||
cmd.Run() // Ignore if already exists
|
||||
// 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=<name> dev=<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=<name> dev=<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=<name> file_or_dev=<path> [size=<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(s.targetcliPath, "/iscsi/"+target.IQN+"/tpg1/luns", "create", zvolPath)
|
||||
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) {
|
||||
@@ -144,7 +292,7 @@ func (s *ISCSIService) configureLUNs(target models.ISCSITarget) error {
|
||||
|
||||
// Helper functions
|
||||
func (s *ISCSIService) targetExists(iqn string) bool {
|
||||
cmd := exec.Command(s.targetcliPath, "/iscsi", "ls")
|
||||
cmd := exec.Command("sudo", "-n", s.targetcliPath, "/iscsi", "ls")
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
return false
|
||||
@@ -153,25 +301,56 @@ func (s *ISCSIService) targetExists(iqn string) bool {
|
||||
}
|
||||
|
||||
func (s *ISCSIService) getACLs(iqn string) ([]string, error) {
|
||||
cmd := exec.Command(s.targetcliPath, "/iscsi/"+iqn+"/tpg1/acls", "ls")
|
||||
_, err := cmd.Output()
|
||||
cmd := exec.Command("sudo", "-n", s.targetcliPath, "/iscsi/"+iqn+"/tpg1/acls", "ls")
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return []string{}, nil // Return empty if can't get ACLs
|
||||
}
|
||||
|
||||
// Parse output to extract ACL names
|
||||
// This is simplified - real implementation would parse targetcli output
|
||||
return []string{}, nil
|
||||
// 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(s.targetcliPath, "/iscsi/"+iqn+"/tpg1/luns", "ls")
|
||||
_, err := cmd.Output()
|
||||
cmd := exec.Command("sudo", "-n", s.targetcliPath, "/iscsi/"+iqn+"/tpg1/luns", "ls")
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return []int{}, nil // Return empty if can't get LUNs
|
||||
}
|
||||
|
||||
// Parse output to extract LUN IDs
|
||||
// This is simplified - real implementation would parse targetcli output
|
||||
return []int{}, nil
|
||||
// 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 {
|
||||
@@ -193,7 +372,7 @@ func (s *ISCSIService) hasLUNID(luns []int, id int) bool {
|
||||
}
|
||||
|
||||
func (s *ISCSIService) disableTarget(iqn string) error {
|
||||
cmd := exec.Command(s.targetcliPath, "/iscsi/"+iqn+"/tpg1", "set", "attribute", "enable=0")
|
||||
cmd := exec.Command("sudo", "-n", s.targetcliPath, "/iscsi/"+iqn+"/tpg1", "set", "attribute", "enable=0")
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
@@ -326,7 +505,7 @@ func (s *ISCSIService) GetConnectionInstructions(target models.ISCSITarget, port
|
||||
// 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")
|
||||
cmd := exec.Command("sudo", "-n", s.targetcliPath, "/iscsi", "ls")
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
// Fallback: try to get system IP
|
||||
|
||||
Reference in New Issue
Block a user