318
docs/ISCSI_CONNECTION.md
Normal file
318
docs/ISCSI_CONNECTION.md
Normal file
@@ -0,0 +1,318 @@
|
|||||||
|
# iSCSI Connection Instructions
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
AtlasOS provides iSCSI connection instructions to help users connect initiators to iSCSI targets. The system automatically generates platform-specific commands for Linux, Windows, and macOS.
|
||||||
|
|
||||||
|
## API Endpoint
|
||||||
|
|
||||||
|
### Get Connection Instructions
|
||||||
|
|
||||||
|
**GET** `/api/v1/iscsi/targets/{id}/connection`
|
||||||
|
|
||||||
|
Returns connection instructions for an iSCSI target, including platform-specific commands.
|
||||||
|
|
||||||
|
**Query Parameters:**
|
||||||
|
- `port` (optional): Portal port number (default: 3260)
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"iqn": "iqn.2024-12.com.atlas:target1",
|
||||||
|
"portal": "192.168.1.100:3260",
|
||||||
|
"portal_ip": "192.168.1.100",
|
||||||
|
"portal_port": 3260,
|
||||||
|
"luns": [
|
||||||
|
{
|
||||||
|
"id": 0,
|
||||||
|
"zvol": "tank/iscsi/lun1",
|
||||||
|
"size": 10737418240
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"commands": {
|
||||||
|
"linux": [
|
||||||
|
"# Discover target",
|
||||||
|
"iscsiadm -m discovery -t sendtargets -p 192.168.1.100:3260",
|
||||||
|
"",
|
||||||
|
"# Login to target",
|
||||||
|
"iscsiadm -m node -T iqn.2024-12.com.atlas:target1 -p 192.168.1.100:3260 --login",
|
||||||
|
"",
|
||||||
|
"# Verify connection",
|
||||||
|
"iscsiadm -m session",
|
||||||
|
"",
|
||||||
|
"# Logout (when done)",
|
||||||
|
"iscsiadm -m node -T iqn.2024-12.com.atlas:target1 -p 192.168.1.100:3260 --logout"
|
||||||
|
],
|
||||||
|
"windows": [
|
||||||
|
"# Open PowerShell as Administrator",
|
||||||
|
"",
|
||||||
|
"# Add iSCSI target portal",
|
||||||
|
"New-IscsiTargetPortal -TargetPortalAddress 192.168.1.100 -TargetPortalPortNumber 3260",
|
||||||
|
"",
|
||||||
|
"# Connect to target",
|
||||||
|
"Connect-IscsiTarget -NodeAddress iqn.2024-12.com.atlas:target1",
|
||||||
|
"",
|
||||||
|
"# Verify connection",
|
||||||
|
"Get-IscsiSession",
|
||||||
|
"",
|
||||||
|
"# Disconnect (when done)",
|
||||||
|
"Disconnect-IscsiTarget -NodeAddress iqn.2024-12.com.atlas:target1"
|
||||||
|
],
|
||||||
|
"macos": [
|
||||||
|
"# macOS uses built-in iSCSI support",
|
||||||
|
"# Use System Preferences > Network > iSCSI",
|
||||||
|
"",
|
||||||
|
"# Or use command line (if iscsiutil is available)",
|
||||||
|
"iscsiutil -a -t iqn.2024-12.com.atlas:target1 -p 192.168.1.100:3260",
|
||||||
|
"",
|
||||||
|
"# Portal: 192.168.1.100:3260",
|
||||||
|
"# Target IQN: iqn.2024-12.com.atlas:target1"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage Examples
|
||||||
|
|
||||||
|
### Get Connection Instructions
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl http://localhost:8080/api/v1/iscsi/targets/iscsi-1/connection \
|
||||||
|
-H "Authorization: Bearer $TOKEN"
|
||||||
|
```
|
||||||
|
|
||||||
|
### With Custom Port
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl "http://localhost:8080/api/v1/iscsi/targets/iscsi-1/connection?port=3261" \
|
||||||
|
-H "Authorization: Bearer $TOKEN"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Platform-Specific Instructions
|
||||||
|
|
||||||
|
### Linux
|
||||||
|
|
||||||
|
**Prerequisites:**
|
||||||
|
- `open-iscsi` package installed
|
||||||
|
- `iscsid` service running
|
||||||
|
|
||||||
|
**Steps:**
|
||||||
|
1. Discover the target
|
||||||
|
2. Login to the target
|
||||||
|
3. Verify connection
|
||||||
|
4. Use the device (appears as `/dev/sdX` or `/dev/disk/by-id/...`)
|
||||||
|
5. Logout when done
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```bash
|
||||||
|
# Discover target
|
||||||
|
iscsiadm -m discovery -t sendtargets -p 192.168.1.100:3260
|
||||||
|
|
||||||
|
# Login to target
|
||||||
|
iscsiadm -m node -T iqn.2024-12.com.atlas:target1 -p 192.168.1.100:3260 --login
|
||||||
|
|
||||||
|
# Verify connection
|
||||||
|
iscsiadm -m session
|
||||||
|
|
||||||
|
# Find device
|
||||||
|
lsblk
|
||||||
|
# or
|
||||||
|
ls -l /dev/disk/by-id/ | grep iqn
|
||||||
|
|
||||||
|
# Logout when done
|
||||||
|
iscsiadm -m node -T iqn.2024-12.com.atlas:target1 -p 192.168.1.100:3260 --logout
|
||||||
|
```
|
||||||
|
|
||||||
|
### Windows
|
||||||
|
|
||||||
|
**Prerequisites:**
|
||||||
|
- Windows 8+ or Windows Server 2012+
|
||||||
|
- PowerShell (run as Administrator)
|
||||||
|
|
||||||
|
**Steps:**
|
||||||
|
1. Add iSCSI target portal
|
||||||
|
2. Connect to target
|
||||||
|
3. Verify connection
|
||||||
|
4. Initialize disk in Disk Management
|
||||||
|
5. Disconnect when done
|
||||||
|
|
||||||
|
**Example (PowerShell as Administrator):**
|
||||||
|
```powershell
|
||||||
|
# Add portal
|
||||||
|
New-IscsiTargetPortal -TargetPortalAddress 192.168.1.100 -TargetPortalPortNumber 3260
|
||||||
|
|
||||||
|
# Connect to target
|
||||||
|
Connect-IscsiTarget -NodeAddress iqn.2024-12.com.atlas:target1
|
||||||
|
|
||||||
|
# Verify connection
|
||||||
|
Get-IscsiSession
|
||||||
|
|
||||||
|
# Initialize disk in Disk Management
|
||||||
|
# (Open Disk Management, find new disk, initialize and format)
|
||||||
|
|
||||||
|
# Disconnect when done
|
||||||
|
Disconnect-IscsiTarget -NodeAddress iqn.2024-12.com.atlas:target1
|
||||||
|
```
|
||||||
|
|
||||||
|
### macOS
|
||||||
|
|
||||||
|
**Prerequisites:**
|
||||||
|
- macOS 10.13+ (High Sierra or later)
|
||||||
|
- iSCSI initiator software (third-party)
|
||||||
|
|
||||||
|
**Steps:**
|
||||||
|
1. Use GUI iSCSI initiator (if available)
|
||||||
|
2. Or use command line tools
|
||||||
|
3. Configure connection settings
|
||||||
|
4. Connect to target
|
||||||
|
|
||||||
|
**Note:** macOS doesn't have built-in iSCSI support. Use third-party software like:
|
||||||
|
- GlobalSAN iSCSI Initiator
|
||||||
|
- ATTO Xtend SAN iSCSI
|
||||||
|
|
||||||
|
## Portal IP Detection
|
||||||
|
|
||||||
|
The system automatically detects the portal IP address using:
|
||||||
|
|
||||||
|
1. **Primary Method**: Parse `targetcli` output to find configured portal IP
|
||||||
|
2. **Fallback Method**: Use system IP from `hostname -I`
|
||||||
|
3. **Default**: `127.0.0.1` if detection fails
|
||||||
|
|
||||||
|
**Custom Portal IP:**
|
||||||
|
|
||||||
|
If the detected IP is incorrect, you can manually specify it by:
|
||||||
|
- Setting environment variable `ATLAS_ISCSI_PORTAL_IP`
|
||||||
|
- Or modifying the connection instructions after retrieval
|
||||||
|
|
||||||
|
## LUN Information
|
||||||
|
|
||||||
|
The connection instructions include LUN information:
|
||||||
|
|
||||||
|
- **ID**: LUN number (typically 0, 1, 2, ...)
|
||||||
|
- **ZVOL**: ZFS volume backing the LUN
|
||||||
|
- **Size**: LUN size in bytes
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```json
|
||||||
|
"luns": [
|
||||||
|
{
|
||||||
|
"id": 0,
|
||||||
|
"zvol": "tank/iscsi/lun1",
|
||||||
|
"size": 10737418240
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"zvol": "tank/iscsi/lun2",
|
||||||
|
"size": 21474836480
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Security Considerations
|
||||||
|
|
||||||
|
### Initiator ACLs
|
||||||
|
|
||||||
|
iSCSI targets can be configured with initiator ACLs to restrict access:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"iqn": "iqn.2024-12.com.atlas:target1",
|
||||||
|
"initiators": [
|
||||||
|
"iqn.2024-12.com.client:initiator1"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Only initiators in the ACL list can connect to the target.
|
||||||
|
|
||||||
|
### CHAP Authentication
|
||||||
|
|
||||||
|
For production deployments, configure CHAP authentication:
|
||||||
|
|
||||||
|
1. Set up CHAP credentials in target configuration
|
||||||
|
2. Configure initiator with matching credentials
|
||||||
|
3. Use authentication in connection commands
|
||||||
|
|
||||||
|
**Note:** CHAP configuration is not yet exposed via API (future enhancement).
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Connection Fails
|
||||||
|
|
||||||
|
1. **Check Target Status**: Verify target is enabled
|
||||||
|
2. **Check Portal**: Verify portal IP and port are correct
|
||||||
|
3. **Check Network**: Ensure network connectivity
|
||||||
|
4. **Check ACLs**: Verify initiator IQN is in ACL list
|
||||||
|
5. **Check Firewall**: Ensure port 3260 (or custom port) is open
|
||||||
|
|
||||||
|
### Portal IP Incorrect
|
||||||
|
|
||||||
|
If the detected portal IP is wrong:
|
||||||
|
|
||||||
|
1. Check `targetcli` configuration
|
||||||
|
2. Verify network interfaces
|
||||||
|
3. Manually override in connection commands
|
||||||
|
|
||||||
|
### LUN Not Visible
|
||||||
|
|
||||||
|
1. **Check LUN Mapping**: Verify LUN is mapped to target
|
||||||
|
2. **Check ZVOL**: Verify ZVOL exists and is accessible
|
||||||
|
3. **Rescan**: Rescan iSCSI session on initiator
|
||||||
|
4. **Check Permissions**: Verify initiator has access
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
### 1. Use ACLs
|
||||||
|
|
||||||
|
Always configure initiator ACLs to restrict access:
|
||||||
|
- Only allow known initiators
|
||||||
|
- Use descriptive initiator IQNs
|
||||||
|
- Regularly review ACL lists
|
||||||
|
|
||||||
|
### 2. Use CHAP Authentication
|
||||||
|
|
||||||
|
For production:
|
||||||
|
- Enable CHAP authentication
|
||||||
|
- Use strong credentials
|
||||||
|
- Rotate credentials regularly
|
||||||
|
|
||||||
|
### 3. Monitor Connections
|
||||||
|
|
||||||
|
- Monitor active iSCSI sessions
|
||||||
|
- Track connection/disconnection events
|
||||||
|
- Set up alerts for connection failures
|
||||||
|
|
||||||
|
### 4. Test Connections
|
||||||
|
|
||||||
|
Before production use:
|
||||||
|
- Test connection from initiator
|
||||||
|
- Verify LUN visibility
|
||||||
|
- Test read/write operations
|
||||||
|
- Test disconnection/reconnection
|
||||||
|
|
||||||
|
### 5. Document Configuration
|
||||||
|
|
||||||
|
- Document portal IPs and ports
|
||||||
|
- Document initiator IQNs
|
||||||
|
- Document LUN mappings
|
||||||
|
- Keep connection instructions accessible
|
||||||
|
|
||||||
|
## Compliance with SRS
|
||||||
|
|
||||||
|
Per SRS section 4.6 iSCSI Block Storage:
|
||||||
|
|
||||||
|
- ✅ **Provision ZVOL-backed LUNs**: Implemented
|
||||||
|
- ✅ **Create iSCSI targets with IQN**: Implemented
|
||||||
|
- ✅ **Map LUNs to targets**: Implemented
|
||||||
|
- ✅ **Configure initiator ACLs**: Implemented
|
||||||
|
- ✅ **Expose connection instructions**: Implemented (Priority 21)
|
||||||
|
|
||||||
|
## Future Enhancements
|
||||||
|
|
||||||
|
1. **CHAP Authentication**: API support for CHAP configuration
|
||||||
|
2. **Portal Management**: Manage multiple portals per target
|
||||||
|
3. **Connection Monitoring**: Real-time connection status
|
||||||
|
4. **Auto-Discovery**: Automatic initiator discovery
|
||||||
|
5. **Connection Templates**: Pre-configured connection templates
|
||||||
|
6. **Connection History**: Track connection/disconnection events
|
||||||
|
7. **Multi-Path Support**: Instructions for multi-path configurations
|
||||||
@@ -958,23 +958,61 @@ func (a *App) handleCreateISCSITarget(w http.ResponseWriter, r *http.Request) {
|
|||||||
func (a *App) handleGetISCSITarget(w http.ResponseWriter, r *http.Request) {
|
func (a *App) handleGetISCSITarget(w http.ResponseWriter, r *http.Request) {
|
||||||
id := pathParam(r, "/api/v1/iscsi/targets/")
|
id := pathParam(r, "/api/v1/iscsi/targets/")
|
||||||
if id == "" {
|
if id == "" {
|
||||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "target id required"})
|
writeError(w, errors.ErrBadRequest("target id required"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
target, err := a.iscsiStore.Get(id)
|
target, err := a.iscsiStore.Get(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err == storage.ErrISCSITargetNotFound {
|
if err == storage.ErrISCSITargetNotFound {
|
||||||
writeJSON(w, http.StatusNotFound, map[string]string{"error": err.Error()})
|
writeError(w, errors.ErrNotFound("iSCSI target"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
writeError(w, errors.ErrInternal("failed to get iSCSI target").WithDetails(err.Error()))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
writeJSON(w, http.StatusOK, target)
|
writeJSON(w, http.StatusOK, target)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *App) handleGetISCSIConnectionInstructions(w http.ResponseWriter, r *http.Request) {
|
||||||
|
id := pathParam(r, "/api/v1/iscsi/targets/")
|
||||||
|
if id == "" {
|
||||||
|
writeError(w, errors.ErrBadRequest("target id required"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
target, err := a.iscsiStore.Get(id)
|
||||||
|
if err != nil {
|
||||||
|
if err == storage.ErrISCSITargetNotFound {
|
||||||
|
writeError(w, errors.ErrNotFound("iSCSI target"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeError(w, errors.ErrInternal("failed to get iSCSI target").WithDetails(err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get portal IP (with fallback)
|
||||||
|
portalIP, err := a.iscsiService.GetPortalIP()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("get portal IP error: %v", err)
|
||||||
|
portalIP = "127.0.0.1" // Fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get portal port from query parameter or use default
|
||||||
|
portalPort := 3260
|
||||||
|
if portStr := r.URL.Query().Get("port"); portStr != "" {
|
||||||
|
if port, err := strconv.Atoi(portStr); err == nil && port > 0 && port < 65536 {
|
||||||
|
portalPort = port
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate connection instructions
|
||||||
|
instructions := a.iscsiService.GetConnectionInstructions(*target, portalIP, portalPort)
|
||||||
|
|
||||||
|
writeJSON(w, http.StatusOK, instructions)
|
||||||
|
}
|
||||||
|
|
||||||
func (a *App) handleUpdateISCSITarget(w http.ResponseWriter, r *http.Request) {
|
func (a *App) handleUpdateISCSITarget(w http.ResponseWriter, r *http.Request) {
|
||||||
id := pathParam(r, "/api/v1/iscsi/targets/")
|
id := pathParam(r, "/api/v1/iscsi/targets/")
|
||||||
if id == "" {
|
if id == "" {
|
||||||
|
|||||||
@@ -193,12 +193,27 @@ func (a *App) handleBackupOps(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) handleISCSITargetOps(w http.ResponseWriter, r *http.Request) {
|
func (a *App) handleISCSITargetOps(w http.ResponseWriter, r *http.Request) {
|
||||||
|
id := pathParam(r, "/api/v1/iscsi/targets/")
|
||||||
|
if id == "" {
|
||||||
|
writeError(w, errors.ErrBadRequest("target id required"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasSuffix(r.URL.Path, "/connection") {
|
||||||
|
if r.Method == http.MethodGet {
|
||||||
|
a.handleGetISCSIConnectionInstructions(w, r)
|
||||||
|
} else {
|
||||||
|
writeError(w, errors.NewAPIError(errors.ErrCodeBadRequest, "method not allowed", http.StatusMethodNotAllowed))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if strings.HasSuffix(r.URL.Path, "/luns") {
|
if strings.HasSuffix(r.URL.Path, "/luns") {
|
||||||
if r.Method == http.MethodPost {
|
if r.Method == http.MethodPost {
|
||||||
a.handleAddLUN(w, r)
|
a.handleAddLUN(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
writeError(w, errors.NewAPIError(errors.ErrCodeBadRequest, "method not allowed", http.StatusMethodNotAllowed))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -207,7 +222,7 @@ func (a *App) handleISCSITargetOps(w http.ResponseWriter, r *http.Request) {
|
|||||||
a.handleRemoveLUN(w, r)
|
a.handleRemoveLUN(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
writeError(w, errors.NewAPIError(errors.ErrCodeBadRequest, "method not allowed", http.StatusMethodNotAllowed))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -214,3 +214,153 @@ func contains(slice []string, item string) bool {
|
|||||||
}
|
}
|
||||||
return false
|
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
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user