367 lines
9.8 KiB
Go
367 lines
9.8 KiB
Go
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 {
|
|
return &ISCSIService{
|
|
targetcliPath: "targetcli",
|
|
}
|
|
}
|
|
|
|
// 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 <IQN>
|
|
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/<IQN>/tpg1/luns create /backstores/zvol/<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
|
|
}
|