Files
atlas/internal/services/iscsi.go
othman.suseno 7ac7e77f1d
Some checks failed
CI / test-build (push) Failing after 2m16s
update installer script
2025-12-15 01:32:41 +07:00

375 lines
10 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 {
// 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 <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
}