1360 lines
40 KiB
Go
1360 lines
40 KiB
Go
package scst
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/atlasos/calypso/internal/common/database"
|
|
"github.com/atlasos/calypso/internal/common/logger"
|
|
)
|
|
|
|
// Service handles SCST operations
|
|
type Service struct {
|
|
db *database.DB
|
|
logger *logger.Logger
|
|
}
|
|
|
|
// NewService creates a new SCST service
|
|
func NewService(db *database.DB, log *logger.Logger) *Service {
|
|
return &Service{
|
|
db: db,
|
|
logger: log,
|
|
}
|
|
}
|
|
|
|
// Target represents an SCST iSCSI target
|
|
type Target struct {
|
|
ID string `json:"id"`
|
|
IQN string `json:"iqn"`
|
|
TargetType string `json:"target_type"` // 'disk', 'vtl', 'physical_tape'
|
|
Name string `json:"name"`
|
|
Description string `json:"description"`
|
|
IsActive bool `json:"is_active"`
|
|
SingleInitiatorOnly bool `json:"single_initiator_only"`
|
|
LUNCount int `json:"lun_count"` // Number of LUNs attached to this target
|
|
CreatedAt time.Time `json:"created_at"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
CreatedBy string `json:"created_by"`
|
|
}
|
|
|
|
// LUN represents an SCST LUN mapping
|
|
type LUN struct {
|
|
ID string `json:"id"`
|
|
TargetID string `json:"target_id"`
|
|
LUNNumber int `json:"lun_number"`
|
|
DeviceName string `json:"device_name"`
|
|
DevicePath string `json:"device_path"`
|
|
HandlerType string `json:"handler_type"`
|
|
Handler string `json:"handler"` // Alias for handler_type for frontend compatibility
|
|
DeviceType string `json:"device_type"` // Derived from handler_type
|
|
IsActive bool `json:"is_active"` // True if LUN exists in SCST
|
|
CreatedAt time.Time `json:"created_at"`
|
|
}
|
|
|
|
// InitiatorGroup represents an SCST initiator group
|
|
type InitiatorGroup struct {
|
|
ID string `json:"id"`
|
|
TargetID string `json:"target_id"`
|
|
GroupName string `json:"group_name"`
|
|
Initiators []Initiator `json:"initiators"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
}
|
|
|
|
// Initiator represents an iSCSI initiator
|
|
type Initiator struct {
|
|
ID string `json:"id"`
|
|
GroupID string `json:"group_id"`
|
|
IQN string `json:"iqn"`
|
|
IsActive bool `json:"is_active"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
}
|
|
|
|
// Portal represents an iSCSI network portal (IP address and port)
|
|
type Portal struct {
|
|
ID string `json:"id"`
|
|
IPAddress string `json:"ip_address"` // IPv4 or IPv6 address, or "0.0.0.0" for all interfaces
|
|
Port int `json:"port"` // Default 3260 for iSCSI
|
|
IsActive bool `json:"is_active"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
}
|
|
|
|
// Extent represents an SCST device extent (opened device)
|
|
type Extent struct {
|
|
HandlerType string `json:"handler_type"`
|
|
DeviceName string `json:"device_name"`
|
|
DevicePath string `json:"device_path"` // Path to the actual device/file
|
|
IsInUse bool `json:"is_in_use"` // True if device is used by any LUN
|
|
LUNCount int `json:"lun_count"` // Number of LUNs using this device
|
|
}
|
|
|
|
// CreateTarget creates a new SCST iSCSI target
|
|
func (s *Service) CreateTarget(ctx context.Context, target *Target) error {
|
|
// Validate IQN format
|
|
if !strings.HasPrefix(target.IQN, "iqn.") {
|
|
return fmt.Errorf("invalid IQN format")
|
|
}
|
|
|
|
// Create target in SCST
|
|
cmd := exec.CommandContext(ctx, "scstadmin", "-add_target", target.IQN, "-driver", "iscsi")
|
|
output, err := cmd.CombinedOutput()
|
|
outputStr := string(output)
|
|
|
|
if err != nil {
|
|
// Check if target already exists
|
|
if strings.Contains(outputStr, "already exists") {
|
|
s.logger.Warn("Target already exists in SCST", "iqn", target.IQN)
|
|
} else {
|
|
// Check for common SCST errors
|
|
if strings.Contains(outputStr, "User space process is not connected") ||
|
|
strings.Contains(outputStr, "iscsi-scstd") {
|
|
return fmt.Errorf("iSCSI daemon (iscsi-scstd) is not running. Please start it with: systemctl start iscsi-scstd")
|
|
}
|
|
if strings.Contains(outputStr, "Failed to add target") {
|
|
return fmt.Errorf("failed to add target to SCST: %s. Check dmesg for details", strings.TrimSpace(outputStr))
|
|
}
|
|
return fmt.Errorf("failed to create SCST target: %s: %w", outputStr, err)
|
|
}
|
|
}
|
|
|
|
// Check for warnings in output even if command succeeded
|
|
if strings.Contains(outputStr, "WARNING") || strings.Contains(outputStr, "Failed to add target") {
|
|
s.logger.Warn("SCST warning during target creation", "iqn", target.IQN, "output", outputStr)
|
|
// Check dmesg for more details
|
|
dmesgCmd := exec.CommandContext(ctx, "dmesg", "-T", "-l", "err,warn")
|
|
if dmesgOutput, dmesgErr := dmesgCmd.CombinedOutput(); dmesgErr == nil {
|
|
recentErrors := string(dmesgOutput)
|
|
if strings.Contains(recentErrors, "iscsi-scst") {
|
|
return fmt.Errorf("iSCSI daemon (iscsi-scstd) is not running. Please start it with: systemctl start iscsi-scstd")
|
|
}
|
|
}
|
|
return fmt.Errorf("target creation completed but SCST reported an error: %s. Check dmesg for details", strings.TrimSpace(outputStr))
|
|
}
|
|
|
|
// Insert into database
|
|
query := `
|
|
INSERT INTO scst_targets (
|
|
iqn, target_type, name, description, is_active,
|
|
single_initiator_only, created_by
|
|
) VALUES ($1, $2, $3, $4, $5, $6, $7)
|
|
RETURNING id, created_at, updated_at
|
|
`
|
|
|
|
err = s.db.QueryRowContext(ctx, query,
|
|
target.IQN, target.TargetType, target.Name, target.Description,
|
|
target.IsActive, target.SingleInitiatorOnly, target.CreatedBy,
|
|
).Scan(&target.ID, &target.CreatedAt, &target.UpdatedAt)
|
|
if err != nil {
|
|
// Rollback: remove from SCST
|
|
exec.CommandContext(ctx, "scstadmin", "-remove_target", target.IQN, "-driver", "iscsi").Run()
|
|
return fmt.Errorf("failed to save target to database: %w", err)
|
|
}
|
|
|
|
s.logger.Info("SCST target created", "iqn", target.IQN, "type", target.TargetType)
|
|
return nil
|
|
}
|
|
|
|
// AddLUN adds a LUN to a target
|
|
func (s *Service) AddLUN(ctx context.Context, targetIQN, deviceName, devicePath string, lunNumber int, handlerType string) error {
|
|
// Open device in SCST
|
|
openCmd := exec.CommandContext(ctx, "scstadmin", "-open_dev", deviceName,
|
|
"-handler", handlerType,
|
|
"-attributes", fmt.Sprintf("filename=%s", devicePath))
|
|
output, err := openCmd.CombinedOutput()
|
|
if err != nil {
|
|
if !strings.Contains(string(output), "already exists") {
|
|
return fmt.Errorf("failed to open device in SCST: %s: %w", string(output), err)
|
|
}
|
|
}
|
|
|
|
// Add LUN to target
|
|
addCmd := exec.CommandContext(ctx, "scstadmin", "-add_lun", fmt.Sprintf("%d", lunNumber),
|
|
"-target", targetIQN,
|
|
"-driver", "iscsi",
|
|
"-device", deviceName)
|
|
output, err = addCmd.CombinedOutput()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to add LUN to target: %s: %w", string(output), err)
|
|
}
|
|
|
|
// Get target ID
|
|
var targetID string
|
|
err = s.db.QueryRowContext(ctx, "SELECT id FROM scst_targets WHERE iqn = $1", targetIQN).Scan(&targetID)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get target ID: %w", err)
|
|
}
|
|
|
|
// Insert into database
|
|
_, err = s.db.ExecContext(ctx, `
|
|
INSERT INTO scst_luns (target_id, lun_number, device_name, device_path, handler_type)
|
|
VALUES ($1, $2, $3, $4, $5)
|
|
ON CONFLICT (target_id, lun_number) DO UPDATE SET
|
|
device_name = EXCLUDED.device_name,
|
|
device_path = EXCLUDED.device_path,
|
|
handler_type = EXCLUDED.handler_type
|
|
`, targetID, lunNumber, deviceName, devicePath, handlerType)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to save LUN to database: %w", err)
|
|
}
|
|
|
|
s.logger.Info("LUN added", "target", targetIQN, "lun", lunNumber, "device", deviceName)
|
|
return nil
|
|
}
|
|
|
|
// AddInitiator adds an initiator to a target
|
|
func (s *Service) AddInitiator(ctx context.Context, targetIQN, initiatorIQN string) error {
|
|
// Get target from database
|
|
var targetID string
|
|
var singleInitiatorOnly bool
|
|
err := s.db.QueryRowContext(ctx,
|
|
"SELECT id, single_initiator_only FROM scst_targets WHERE iqn = $1",
|
|
targetIQN,
|
|
).Scan(&targetID, &singleInitiatorOnly)
|
|
if err != nil {
|
|
return fmt.Errorf("target not found: %w", err)
|
|
}
|
|
|
|
// Check single initiator policy
|
|
if singleInitiatorOnly {
|
|
var existingCount int
|
|
s.db.QueryRowContext(ctx,
|
|
"SELECT COUNT(*) FROM scst_initiators WHERE group_id IN (SELECT id FROM scst_initiator_groups WHERE target_id = $1)",
|
|
targetID,
|
|
).Scan(&existingCount)
|
|
if existingCount > 0 {
|
|
return fmt.Errorf("target enforces single initiator only")
|
|
}
|
|
}
|
|
|
|
// Get or create initiator group
|
|
var groupID string
|
|
groupName := targetIQN + "_acl"
|
|
err = s.db.QueryRowContext(ctx,
|
|
"SELECT id FROM scst_initiator_groups WHERE target_id = $1 AND group_name = $2",
|
|
targetID, groupName,
|
|
).Scan(&groupID)
|
|
|
|
if err == sql.ErrNoRows {
|
|
// Create group in SCST
|
|
cmd := exec.CommandContext(ctx, "scstadmin", "-add_group", groupName,
|
|
"-target", targetIQN,
|
|
"-driver", "iscsi")
|
|
output, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create initiator group: %s: %w", string(output), err)
|
|
}
|
|
|
|
// Insert into database
|
|
err = s.db.QueryRowContext(ctx,
|
|
"INSERT INTO scst_initiator_groups (target_id, group_name) VALUES ($1, $2) RETURNING id",
|
|
targetID, groupName,
|
|
).Scan(&groupID)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to save group to database: %w", err)
|
|
}
|
|
} else if err != nil {
|
|
return fmt.Errorf("failed to get initiator group: %w", err)
|
|
}
|
|
|
|
// Add initiator to group in SCST
|
|
cmd := exec.CommandContext(ctx, "scstadmin", "-add_init", initiatorIQN,
|
|
"-group", groupName,
|
|
"-target", targetIQN,
|
|
"-driver", "iscsi")
|
|
output, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to add initiator: %s: %w", string(output), err)
|
|
}
|
|
|
|
// Insert into database
|
|
_, err = s.db.ExecContext(ctx, `
|
|
INSERT INTO scst_initiators (group_id, iqn, is_active)
|
|
VALUES ($1, $2, true)
|
|
ON CONFLICT (group_id, iqn) DO UPDATE SET is_active = true
|
|
`, groupID, initiatorIQN)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to save initiator to database: %w", err)
|
|
}
|
|
|
|
s.logger.Info("Initiator added", "target", targetIQN, "initiator", initiatorIQN)
|
|
return nil
|
|
}
|
|
|
|
// GetTargetInitiatorGroups retrieves all initiator groups for a target
|
|
func (s *Service) GetTargetInitiatorGroups(ctx context.Context, targetID string) ([]InitiatorGroup, error) {
|
|
// Get all groups for this target
|
|
query := `
|
|
SELECT id, target_id, group_name, created_at
|
|
FROM scst_initiator_groups
|
|
WHERE target_id = $1
|
|
ORDER BY group_name
|
|
`
|
|
|
|
rows, err := s.db.QueryContext(ctx, query, targetID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get initiator groups: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
var groups []InitiatorGroup
|
|
for rows.Next() {
|
|
var group InitiatorGroup
|
|
err := rows.Scan(&group.ID, &group.TargetID, &group.GroupName, &group.CreatedAt)
|
|
if err != nil {
|
|
s.logger.Error("Failed to scan initiator group", "error", err)
|
|
continue
|
|
}
|
|
|
|
// Get initiators for this group
|
|
initiators, err := s.getGroupInitiators(ctx, group.ID)
|
|
if err != nil {
|
|
s.logger.Warn("Failed to get initiators for group", "group_id", group.ID, "error", err)
|
|
group.Initiators = []Initiator{}
|
|
} else {
|
|
group.Initiators = initiators
|
|
}
|
|
|
|
groups = append(groups, group)
|
|
}
|
|
|
|
return groups, rows.Err()
|
|
}
|
|
|
|
// InitiatorWithTarget extends Initiator with target information
|
|
type InitiatorWithTarget struct {
|
|
Initiator
|
|
TargetID string `json:"target_id"`
|
|
TargetIQN string `json:"target_iqn"`
|
|
TargetName string `json:"target_name"`
|
|
GroupName string `json:"group_name"`
|
|
}
|
|
|
|
// ListAllInitiators lists all initiators across all targets
|
|
func (s *Service) ListAllInitiators(ctx context.Context) ([]InitiatorWithTarget, error) {
|
|
query := `
|
|
SELECT i.id, i.group_id, i.iqn, i.is_active, i.created_at,
|
|
ig.target_id, ig.group_name, t.iqn as target_iqn, t.name as target_name
|
|
FROM scst_initiators i
|
|
JOIN scst_initiator_groups ig ON i.group_id = ig.id
|
|
JOIN scst_targets t ON ig.target_id = t.id
|
|
ORDER BY t.iqn, ig.group_name, i.iqn
|
|
`
|
|
|
|
rows, err := s.db.QueryContext(ctx, query)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to list initiators: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
var initiators []InitiatorWithTarget
|
|
for rows.Next() {
|
|
var initiator InitiatorWithTarget
|
|
err := rows.Scan(
|
|
&initiator.ID, &initiator.GroupID, &initiator.IQN,
|
|
&initiator.IsActive, &initiator.CreatedAt,
|
|
&initiator.TargetID, &initiator.GroupName, &initiator.TargetIQN, &initiator.TargetName,
|
|
)
|
|
if err != nil {
|
|
s.logger.Error("Failed to scan initiator", "error", err)
|
|
continue
|
|
}
|
|
initiators = append(initiators, initiator)
|
|
}
|
|
|
|
return initiators, rows.Err()
|
|
}
|
|
|
|
// RemoveInitiator removes an initiator from a target
|
|
func (s *Service) RemoveInitiator(ctx context.Context, initiatorID string) error {
|
|
// Get initiator info
|
|
var initiatorIQN, groupName, targetIQN string
|
|
err := s.db.QueryRowContext(ctx, `
|
|
SELECT i.iqn, ig.group_name, t.iqn
|
|
FROM scst_initiators i
|
|
JOIN scst_initiator_groups ig ON i.group_id = ig.id
|
|
JOIN scst_targets t ON ig.target_id = t.id
|
|
WHERE i.id = $1
|
|
`, initiatorID).Scan(&initiatorIQN, &groupName, &targetIQN)
|
|
if err != nil {
|
|
if err == sql.ErrNoRows {
|
|
return fmt.Errorf("initiator not found")
|
|
}
|
|
return fmt.Errorf("failed to get initiator: %w", err)
|
|
}
|
|
|
|
// Remove from SCST
|
|
cmd := exec.CommandContext(ctx, "scstadmin", "-remove_init", initiatorIQN,
|
|
"-group", groupName,
|
|
"-target", targetIQN,
|
|
"-driver", "iscsi")
|
|
output, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
outputStr := string(output)
|
|
if !strings.Contains(outputStr, "not found") {
|
|
return fmt.Errorf("failed to remove initiator from SCST: %s: %w", outputStr, err)
|
|
}
|
|
// If not found in SCST, continue to remove from database
|
|
}
|
|
|
|
// Remove from database
|
|
_, err = s.db.ExecContext(ctx, "DELETE FROM scst_initiators WHERE id = $1", initiatorID)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to remove initiator from database: %w", err)
|
|
}
|
|
|
|
s.logger.Info("Initiator removed", "initiator", initiatorIQN, "target", targetIQN)
|
|
return nil
|
|
}
|
|
|
|
// GetInitiator retrieves an initiator by ID with full details
|
|
func (s *Service) GetInitiator(ctx context.Context, initiatorID string) (*Initiator, error) {
|
|
query := `
|
|
SELECT i.id, i.group_id, i.iqn, i.is_active, i.created_at,
|
|
ig.target_id, ig.group_name, t.iqn as target_iqn, t.name as target_name
|
|
FROM scst_initiators i
|
|
JOIN scst_initiator_groups ig ON i.group_id = ig.id
|
|
JOIN scst_targets t ON ig.target_id = t.id
|
|
WHERE i.id = $1
|
|
`
|
|
|
|
var initiator Initiator
|
|
var targetID, groupName, targetIQN, targetName string
|
|
err := s.db.QueryRowContext(ctx, query, initiatorID).Scan(
|
|
&initiator.ID, &initiator.GroupID, &initiator.IQN,
|
|
&initiator.IsActive, &initiator.CreatedAt,
|
|
&targetID, &groupName, &targetIQN, &targetName,
|
|
)
|
|
if err != nil {
|
|
if err == sql.ErrNoRows {
|
|
return nil, fmt.Errorf("initiator not found")
|
|
}
|
|
return nil, fmt.Errorf("failed to get initiator: %w", err)
|
|
}
|
|
|
|
return &initiator, nil
|
|
}
|
|
|
|
// getGroupInitiators retrieves all initiators for a group
|
|
func (s *Service) getGroupInitiators(ctx context.Context, groupID string) ([]Initiator, error) {
|
|
query := `
|
|
SELECT id, group_id, iqn, is_active, created_at
|
|
FROM scst_initiators
|
|
WHERE group_id = $1
|
|
ORDER BY iqn
|
|
`
|
|
|
|
rows, err := s.db.QueryContext(ctx, query, groupID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get initiators: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
var initiators []Initiator
|
|
for rows.Next() {
|
|
var initiator Initiator
|
|
err := rows.Scan(&initiator.ID, &initiator.GroupID, &initiator.IQN, &initiator.IsActive, &initiator.CreatedAt)
|
|
if err != nil {
|
|
s.logger.Error("Failed to scan initiator", "error", err)
|
|
continue
|
|
}
|
|
initiators = append(initiators, initiator)
|
|
}
|
|
|
|
return initiators, rows.Err()
|
|
}
|
|
|
|
// ListTargets lists all SCST targets
|
|
func (s *Service) ListTargets(ctx context.Context) ([]Target, error) {
|
|
query := `
|
|
SELECT
|
|
t.id, t.iqn, t.target_type, t.name, t.description, t.is_active,
|
|
t.single_initiator_only, t.created_at, t.updated_at, t.created_by,
|
|
COALESCE(COUNT(l.id), 0) as lun_count
|
|
FROM scst_targets t
|
|
LEFT JOIN scst_luns l ON t.id = l.target_id
|
|
GROUP BY t.id, t.iqn, t.target_type, t.name, t.description, t.is_active,
|
|
t.single_initiator_only, t.created_at, t.updated_at, t.created_by
|
|
ORDER BY t.name
|
|
`
|
|
|
|
rows, err := s.db.QueryContext(ctx, query)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to list targets: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
var targets []Target
|
|
for rows.Next() {
|
|
var target Target
|
|
var description sql.NullString
|
|
err := rows.Scan(
|
|
&target.ID, &target.IQN, &target.TargetType, &target.Name,
|
|
&description, &target.IsActive, &target.SingleInitiatorOnly,
|
|
&target.CreatedAt, &target.UpdatedAt, &target.CreatedBy,
|
|
&target.LUNCount,
|
|
)
|
|
if err != nil {
|
|
s.logger.Error("Failed to scan target", "error", err)
|
|
continue
|
|
}
|
|
if description.Valid {
|
|
target.Description = description.String
|
|
}
|
|
targets = append(targets, target)
|
|
}
|
|
|
|
return targets, rows.Err()
|
|
}
|
|
|
|
// GetTarget retrieves a target by ID
|
|
func (s *Service) GetTarget(ctx context.Context, id string) (*Target, error) {
|
|
query := `
|
|
SELECT id, iqn, target_type, name, description, is_active,
|
|
single_initiator_only, created_at, updated_at, created_by
|
|
FROM scst_targets
|
|
WHERE id = $1
|
|
`
|
|
|
|
var target Target
|
|
var description sql.NullString
|
|
err := s.db.QueryRowContext(ctx, query, id).Scan(
|
|
&target.ID, &target.IQN, &target.TargetType, &target.Name,
|
|
&description, &target.IsActive, &target.SingleInitiatorOnly,
|
|
&target.CreatedAt, &target.UpdatedAt, &target.CreatedBy,
|
|
)
|
|
if err != nil {
|
|
if err == sql.ErrNoRows {
|
|
return nil, fmt.Errorf("target not found")
|
|
}
|
|
return nil, fmt.Errorf("failed to get target: %w", err)
|
|
}
|
|
|
|
if description.Valid {
|
|
target.Description = description.String
|
|
}
|
|
|
|
// Sync enabled status from SCST
|
|
enabled, err := s.getTargetEnabledStatus(ctx, target.IQN)
|
|
if err == nil {
|
|
// Update database if status differs
|
|
if enabled != target.IsActive {
|
|
_, err = s.db.ExecContext(ctx, "UPDATE scst_targets SET is_active = $1 WHERE id = $2", enabled, target.ID)
|
|
if err != nil {
|
|
s.logger.Warn("Failed to update target status", "error", err)
|
|
} else {
|
|
target.IsActive = enabled
|
|
}
|
|
}
|
|
}
|
|
|
|
return &target, nil
|
|
}
|
|
|
|
// getTargetEnabledStatus reads enabled status from SCST
|
|
func (s *Service) getTargetEnabledStatus(ctx context.Context, targetIQN string) (bool, error) {
|
|
// Read SCST config to check if target is enabled
|
|
cmd := exec.CommandContext(ctx, "scstadmin", "-write_config", "/tmp/scst_target_check.conf")
|
|
output, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
return false, fmt.Errorf("failed to write config: %s: %w", string(output), err)
|
|
}
|
|
|
|
// Read config file
|
|
configData, err := os.ReadFile("/tmp/scst_target_check.conf")
|
|
if err != nil {
|
|
return false, fmt.Errorf("failed to read config: %w", err)
|
|
}
|
|
|
|
// Check if target is enabled in config
|
|
// Format: TARGET iqn.2025-12.id.atlas:lun01 { enabled 1 } or { enabled 0 }
|
|
configStr := string(configData)
|
|
targetSection := fmt.Sprintf("TARGET %s", targetIQN)
|
|
if !strings.Contains(configStr, targetSection) {
|
|
return false, nil
|
|
}
|
|
|
|
// Extract enabled status
|
|
lines := strings.Split(configStr, "\n")
|
|
inTargetSection := false
|
|
for _, line := range lines {
|
|
line = strings.TrimSpace(line)
|
|
if strings.Contains(line, targetSection) {
|
|
inTargetSection = true
|
|
continue
|
|
}
|
|
if inTargetSection {
|
|
if strings.Contains(line, "enabled 1") {
|
|
return true, nil
|
|
}
|
|
if strings.Contains(line, "enabled 0") {
|
|
return false, nil
|
|
}
|
|
if strings.HasPrefix(line, "TARGET") {
|
|
// Next target, stop searching
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
// Default to enabled if target exists but status not found
|
|
return true, nil
|
|
}
|
|
|
|
// EnableTarget enables a target in SCST
|
|
func (s *Service) EnableTarget(ctx context.Context, targetIQN string) error {
|
|
cmd := exec.CommandContext(ctx, "scstadmin", "-enable_target", targetIQN, "-driver", "iscsi")
|
|
output, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
outputStr := string(output)
|
|
if strings.Contains(outputStr, "already enabled") {
|
|
return nil // Already enabled, no error
|
|
}
|
|
return fmt.Errorf("failed to enable target: %s: %w", outputStr, err)
|
|
}
|
|
|
|
// Update database
|
|
var targetID string
|
|
err = s.db.QueryRowContext(ctx, "SELECT id FROM scst_targets WHERE iqn = $1", targetIQN).Scan(&targetID)
|
|
if err == nil {
|
|
s.db.ExecContext(ctx, "UPDATE scst_targets SET is_active = true WHERE id = $1", targetID)
|
|
}
|
|
|
|
s.logger.Info("Target enabled", "iqn", targetIQN)
|
|
return nil
|
|
}
|
|
|
|
// DisableTarget disables a target in SCST
|
|
func (s *Service) DisableTarget(ctx context.Context, targetIQN string) error {
|
|
cmd := exec.CommandContext(ctx, "scstadmin", "-disable_target", targetIQN, "-driver", "iscsi")
|
|
output, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
outputStr := string(output)
|
|
if strings.Contains(outputStr, "already disabled") {
|
|
return nil // Already disabled, no error
|
|
}
|
|
return fmt.Errorf("failed to disable target: %s: %w", outputStr, err)
|
|
}
|
|
|
|
// Update database
|
|
var targetID string
|
|
err = s.db.QueryRowContext(ctx, "SELECT id FROM scst_targets WHERE iqn = $1", targetIQN).Scan(&targetID)
|
|
if err == nil {
|
|
s.db.ExecContext(ctx, "UPDATE scst_targets SET is_active = false WHERE id = $1", targetID)
|
|
}
|
|
|
|
s.logger.Info("Target disabled", "iqn", targetIQN)
|
|
return nil
|
|
}
|
|
|
|
// GetTargetLUNs retrieves all LUNs for a target
|
|
// It reads from SCST first, then syncs with database
|
|
func (s *Service) GetTargetLUNs(ctx context.Context, targetID string) ([]LUN, error) {
|
|
// Get target IQN
|
|
var targetIQN string
|
|
err := s.db.QueryRowContext(ctx, "SELECT iqn FROM scst_targets WHERE id = $1", targetID).Scan(&targetIQN)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get target IQN: %w", err)
|
|
}
|
|
|
|
// Read LUNs from SCST
|
|
scstLUNs, err := s.readLUNsFromSCST(ctx, targetIQN)
|
|
if err != nil {
|
|
s.logger.Warn("Failed to read LUNs from SCST, using database only", "error", err)
|
|
// Fallback to database only
|
|
return s.getLUNsFromDatabase(ctx, targetID)
|
|
}
|
|
|
|
// Sync SCST LUNs with database
|
|
for _, scstLUN := range scstLUNs {
|
|
// Get device info from SCST
|
|
devicePath, handlerType, err := s.getDeviceInfo(ctx, scstLUN.DeviceName)
|
|
if err != nil {
|
|
s.logger.Warn("Failed to get device info", "device", scstLUN.DeviceName, "error", err)
|
|
// Try to get from existing database record if available
|
|
var existingPath, existingHandler string
|
|
s.db.QueryRowContext(ctx,
|
|
"SELECT device_path, handler_type FROM scst_luns WHERE target_id = $1 AND lun_number = $2",
|
|
targetID, scstLUN.LUNNumber,
|
|
).Scan(&existingPath, &existingHandler)
|
|
if existingPath != "" {
|
|
devicePath = existingPath
|
|
}
|
|
if existingHandler != "" {
|
|
handlerType = existingHandler
|
|
}
|
|
}
|
|
|
|
// Upsert into database
|
|
_, err = s.db.ExecContext(ctx, `
|
|
INSERT INTO scst_luns (target_id, lun_number, device_name, device_path, handler_type)
|
|
VALUES ($1, $2, $3, $4, $5)
|
|
ON CONFLICT (target_id, lun_number) DO UPDATE SET
|
|
device_name = EXCLUDED.device_name,
|
|
device_path = COALESCE(NULLIF(EXCLUDED.device_path, ''), scst_luns.device_path),
|
|
handler_type = COALESCE(NULLIF(EXCLUDED.handler_type, ''), scst_luns.handler_type)
|
|
`, targetID, scstLUN.LUNNumber, scstLUN.DeviceName, devicePath, handlerType)
|
|
if err != nil {
|
|
s.logger.Warn("Failed to sync LUN to database", "lun", scstLUN.LUNNumber, "error", err)
|
|
}
|
|
}
|
|
|
|
// Return from database (now synced)
|
|
return s.getLUNsFromDatabase(ctx, targetID)
|
|
}
|
|
|
|
// getLUNsFromDatabase retrieves LUNs from database only
|
|
func (s *Service) getLUNsFromDatabase(ctx context.Context, targetID string) ([]LUN, error) {
|
|
query := `
|
|
SELECT id, target_id, lun_number, device_name, device_path, handler_type, created_at
|
|
FROM scst_luns
|
|
WHERE target_id = $1
|
|
ORDER BY lun_number
|
|
`
|
|
|
|
rows, err := s.db.QueryContext(ctx, query, targetID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get LUNs: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
var luns []LUN
|
|
for rows.Next() {
|
|
var lun LUN
|
|
err := rows.Scan(
|
|
&lun.ID, &lun.TargetID, &lun.LUNNumber, &lun.DeviceName,
|
|
&lun.DevicePath, &lun.HandlerType, &lun.CreatedAt,
|
|
)
|
|
if err != nil {
|
|
s.logger.Error("Failed to scan LUN", "error", err)
|
|
continue
|
|
}
|
|
|
|
// Set handler and device_type for frontend compatibility
|
|
lun.Handler = lun.HandlerType
|
|
// Map handler type to user-friendly device type label
|
|
lun.DeviceType = s.getDeviceTypeLabel(lun.HandlerType)
|
|
// LUN is active if it exists in database (we sync from SCST, so if it's here, it's active)
|
|
lun.IsActive = true
|
|
|
|
luns = append(luns, lun)
|
|
}
|
|
|
|
return luns, rows.Err()
|
|
}
|
|
|
|
// readLUNsFromSCST reads LUNs directly from SCST using scstadmin -list_group
|
|
func (s *Service) readLUNsFromSCST(ctx context.Context, targetIQN string) ([]LUN, error) {
|
|
cmd := exec.CommandContext(ctx, "scstadmin", "-list_group",
|
|
"-target", targetIQN,
|
|
"-driver", "iscsi")
|
|
output, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to list group: %s: %w", string(output), err)
|
|
}
|
|
|
|
// Parse output
|
|
// Format:
|
|
// Driver: iscsi
|
|
// Target: iqn.2025-12.id.atlas:lun01
|
|
//
|
|
// Assigned LUNs:
|
|
//
|
|
// LUN Device
|
|
// ----------
|
|
// 1 LUN01
|
|
|
|
lines := strings.Split(string(output), "\n")
|
|
var luns []LUN
|
|
inLUNSection := false
|
|
|
|
for _, line := range lines {
|
|
line = strings.TrimSpace(line)
|
|
|
|
// Check if we're in the LUN section
|
|
if strings.Contains(line, "Assigned LUNs:") {
|
|
inLUNSection = true
|
|
continue
|
|
}
|
|
|
|
// Skip separator line
|
|
if strings.HasPrefix(line, "---") {
|
|
continue
|
|
}
|
|
|
|
// Skip header line "LUN Device"
|
|
if strings.HasPrefix(line, "LUN") && strings.Contains(line, "Device") {
|
|
continue
|
|
}
|
|
|
|
// Parse LUN lines (format: "1 LUN01")
|
|
if inLUNSection && line != "" {
|
|
parts := strings.Fields(line)
|
|
if len(parts) >= 2 {
|
|
var lunNumber int
|
|
if _, err := fmt.Sscanf(parts[0], "%d", &lunNumber); err == nil {
|
|
deviceName := parts[1]
|
|
luns = append(luns, LUN{
|
|
LUNNumber: lunNumber,
|
|
DeviceName: deviceName,
|
|
DevicePath: "", // Will be filled by getDeviceInfo
|
|
HandlerType: "", // Will be filled by getDeviceInfo
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
// Stop if we hit "All done"
|
|
if strings.Contains(line, "All done") {
|
|
break
|
|
}
|
|
}
|
|
|
|
return luns, nil
|
|
}
|
|
|
|
// getDeviceInfo gets device path and handler type from SCST
|
|
// Since scstadmin doesn't provide easy access to device attributes,
|
|
// we try to get handler type from device list, and device path from database if available
|
|
func (s *Service) getDeviceInfo(ctx context.Context, deviceName string) (string, string, error) {
|
|
// Find which handler this device belongs to
|
|
listCmd := exec.CommandContext(ctx, "scstadmin", "-list_device")
|
|
output, err := listCmd.Output()
|
|
if err != nil {
|
|
return "", "", fmt.Errorf("failed to list devices: %w", err)
|
|
}
|
|
|
|
// Parse output to find handler
|
|
// Format:
|
|
// Handler Device
|
|
// -----------------------
|
|
// vdisk_fileio LUN01
|
|
lines := strings.Split(string(output), "\n")
|
|
var handlerType string
|
|
inHandlerSection := false
|
|
|
|
for _, line := range lines {
|
|
line = strings.TrimSpace(line)
|
|
|
|
// Check if we're in the handler section
|
|
if strings.Contains(line, "Handler") && strings.Contains(line, "Device") {
|
|
inHandlerSection = true
|
|
continue
|
|
}
|
|
|
|
// Skip separator
|
|
if strings.HasPrefix(line, "---") {
|
|
continue
|
|
}
|
|
|
|
// Parse handler and device lines
|
|
if inHandlerSection && line != "" && !strings.Contains(line, "Collecting") && !strings.Contains(line, "All done") {
|
|
parts := strings.Fields(line)
|
|
if len(parts) >= 2 {
|
|
handler := parts[0]
|
|
device := parts[1]
|
|
if device == deviceName {
|
|
handlerType = handler
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
if strings.Contains(line, "All done") {
|
|
break
|
|
}
|
|
}
|
|
|
|
if handlerType == "" {
|
|
return "", "", fmt.Errorf("handler not found for device %s", deviceName)
|
|
}
|
|
|
|
// Try to get device path from SCST config file
|
|
devicePath := s.getDevicePathFromConfig(deviceName, handlerType)
|
|
|
|
return devicePath, handlerType, nil
|
|
}
|
|
|
|
// getDevicePathFromConfig reads device path from SCST config file
|
|
func (s *Service) getDevicePathFromConfig(deviceName, handlerType string) string {
|
|
// Write current config to temp file
|
|
cmd := exec.Command("scstadmin", "-write_config", "/tmp/scst_device_info.conf")
|
|
if err := cmd.Run(); err != nil {
|
|
s.logger.Warn("Failed to write SCST config", "error", err)
|
|
return ""
|
|
}
|
|
|
|
// Read config file
|
|
configData, err := os.ReadFile("/tmp/scst_device_info.conf")
|
|
if err != nil {
|
|
s.logger.Warn("Failed to read SCST config", "error", err)
|
|
return ""
|
|
}
|
|
|
|
// Parse config to find device
|
|
// Format:
|
|
// DEVICE deviceName {
|
|
// filename /path/to/device
|
|
// }
|
|
lines := strings.Split(string(configData), "\n")
|
|
inDeviceBlock := false
|
|
var devicePath string
|
|
|
|
for _, line := range lines {
|
|
line = strings.TrimSpace(line)
|
|
|
|
// Check if we're entering the device block
|
|
if strings.Contains(line, "DEVICE") && strings.Contains(line, deviceName) {
|
|
inDeviceBlock = true
|
|
continue
|
|
}
|
|
|
|
// Check if we're leaving the device block
|
|
if inDeviceBlock && strings.HasPrefix(line, "}") {
|
|
break
|
|
}
|
|
|
|
// Look for filename in device block
|
|
if inDeviceBlock && strings.Contains(line, "filename") {
|
|
parts := strings.Fields(line)
|
|
for j, part := range parts {
|
|
if part == "filename" && j+1 < len(parts) {
|
|
devicePath = parts[j+1]
|
|
break
|
|
}
|
|
}
|
|
if devicePath != "" {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
return devicePath
|
|
}
|
|
|
|
// ListExtents lists all device extents (opened devices) in SCST
|
|
func (s *Service) ListExtents(ctx context.Context) ([]Extent, error) {
|
|
// List all devices from SCST
|
|
cmd := exec.CommandContext(ctx, "scstadmin", "-list_device")
|
|
output, err := cmd.Output()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to list devices: %w", err)
|
|
}
|
|
|
|
// Parse output
|
|
// Format:
|
|
// Handler Device
|
|
// -----------------------
|
|
// vdisk_fileio LUN01
|
|
lines := strings.Split(string(output), "\n")
|
|
var extents []Extent
|
|
inHandlerSection := false
|
|
|
|
for _, line := range lines {
|
|
line = strings.TrimSpace(line)
|
|
|
|
// Check if we're in the handler section
|
|
if strings.Contains(line, "Handler") && strings.Contains(line, "Device") {
|
|
inHandlerSection = true
|
|
continue
|
|
}
|
|
|
|
// Skip separator
|
|
if strings.HasPrefix(line, "---") {
|
|
continue
|
|
}
|
|
|
|
// Skip header/footer lines
|
|
if strings.Contains(line, "Collecting") || strings.Contains(line, "All done") {
|
|
continue
|
|
}
|
|
|
|
// Parse handler and device lines
|
|
if inHandlerSection && line != "" {
|
|
parts := strings.Fields(line)
|
|
if len(parts) >= 2 {
|
|
handlerType := parts[0]
|
|
deviceName := parts[1]
|
|
|
|
// Skip if device is "-" (no device opened for this handler)
|
|
if deviceName == "-" {
|
|
continue
|
|
}
|
|
|
|
// Get device path from config or database
|
|
devicePath := s.getDevicePathFromConfig(deviceName, handlerType)
|
|
if devicePath == "" {
|
|
// Try to get from database
|
|
var dbPath string
|
|
s.db.QueryRowContext(ctx,
|
|
"SELECT device_path FROM scst_luns WHERE device_name = $1 LIMIT 1",
|
|
deviceName,
|
|
).Scan(&dbPath)
|
|
if dbPath != "" {
|
|
devicePath = dbPath
|
|
}
|
|
}
|
|
|
|
// Count how many LUNs use this device
|
|
var lunCount int
|
|
s.db.QueryRowContext(ctx,
|
|
"SELECT COUNT(*) FROM scst_luns WHERE device_name = $1",
|
|
deviceName,
|
|
).Scan(&lunCount)
|
|
|
|
extents = append(extents, Extent{
|
|
HandlerType: handlerType,
|
|
DeviceName: deviceName,
|
|
DevicePath: devicePath,
|
|
IsInUse: lunCount > 0,
|
|
LUNCount: lunCount,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
return extents, nil
|
|
}
|
|
|
|
// CreateExtent opens a device in SCST (creates an extent)
|
|
func (s *Service) CreateExtent(ctx context.Context, deviceName, devicePath, handlerType string) error {
|
|
// Validate handler type
|
|
handlers, err := s.DetectHandlers(ctx)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to detect handlers: %w", err)
|
|
}
|
|
|
|
handlerValid := false
|
|
for _, h := range handlers {
|
|
if h.Name == handlerType {
|
|
handlerValid = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if !handlerValid {
|
|
return fmt.Errorf("invalid handler type: %s", handlerType)
|
|
}
|
|
|
|
// Open device in SCST
|
|
openCmd := exec.CommandContext(ctx, "scstadmin", "-open_dev", deviceName,
|
|
"-handler", handlerType,
|
|
"-attributes", fmt.Sprintf("filename=%s", devicePath))
|
|
output, err := openCmd.CombinedOutput()
|
|
if err != nil {
|
|
outputStr := string(output)
|
|
if strings.Contains(outputStr, "already exists") {
|
|
return fmt.Errorf("device %s already exists", deviceName)
|
|
}
|
|
return fmt.Errorf("failed to open device: %s: %w", outputStr, err)
|
|
}
|
|
|
|
s.logger.Info("Extent created", "device", deviceName, "handler", handlerType, "path", devicePath)
|
|
return nil
|
|
}
|
|
|
|
// DeleteExtent closes a device in SCST (removes an extent)
|
|
func (s *Service) DeleteExtent(ctx context.Context, deviceName string) error {
|
|
// Check if device is in use by any LUN
|
|
var lunCount int
|
|
err := s.db.QueryRowContext(ctx,
|
|
"SELECT COUNT(*) FROM scst_luns WHERE device_name = $1",
|
|
deviceName,
|
|
).Scan(&lunCount)
|
|
if err == nil && lunCount > 0 {
|
|
return fmt.Errorf("device %s is in use by %d LUN(s). Remove LUNs first", deviceName, lunCount)
|
|
}
|
|
|
|
// Close device in SCST
|
|
closeCmd := exec.CommandContext(ctx, "scstadmin", "-close_dev", deviceName)
|
|
output, err := closeCmd.CombinedOutput()
|
|
if err != nil {
|
|
outputStr := string(output)
|
|
if strings.Contains(outputStr, "not found") {
|
|
return fmt.Errorf("device %s not found", deviceName)
|
|
}
|
|
return fmt.Errorf("failed to close device: %s: %w", outputStr, err)
|
|
}
|
|
|
|
s.logger.Info("Extent deleted", "device", deviceName)
|
|
return nil
|
|
}
|
|
|
|
// getDeviceTypeLabel returns a user-friendly label for device type based on handler type
|
|
func (s *Service) getDeviceTypeLabel(handlerType string) string {
|
|
deviceTypeMap := map[string]string{
|
|
"vdisk_fileio": "ZFS Volume",
|
|
"vdisk_blockio": "Block Device",
|
|
"vdisk_nullio": "Null Device",
|
|
"vcdrom": "CDROM",
|
|
"dev_cdrom": "CDROM",
|
|
"dev_disk": "Physical Disk",
|
|
"dev_disk_perf": "Physical Disk (Performance)",
|
|
}
|
|
|
|
if label, ok := deviceTypeMap[handlerType]; ok {
|
|
return label
|
|
}
|
|
|
|
// Default: return handler type as-is
|
|
return handlerType
|
|
}
|
|
|
|
// WriteConfig writes SCST configuration to file
|
|
func (s *Service) WriteConfig(ctx context.Context, configPath string) error {
|
|
cmd := exec.CommandContext(ctx, "scstadmin", "-write_config", configPath)
|
|
output, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to write SCST config: %s: %w", string(output), err)
|
|
}
|
|
|
|
s.logger.Info("SCST configuration written", "path", configPath)
|
|
return nil
|
|
}
|
|
|
|
// HandlerInfo represents SCST handler information
|
|
type HandlerInfo struct {
|
|
Name string `json:"name"`
|
|
Label string `json:"label"` // Simple, user-friendly label
|
|
Description string `json:"description,omitempty"`
|
|
}
|
|
|
|
// DetectHandlers detects available SCST handlers
|
|
func (s *Service) DetectHandlers(ctx context.Context) ([]HandlerInfo, error) {
|
|
cmd := exec.CommandContext(ctx, "scstadmin", "-list_handler")
|
|
output, err := cmd.Output()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to list handlers: %w", err)
|
|
}
|
|
|
|
// Parse output - skip header lines and separator
|
|
handlers := []HandlerInfo{}
|
|
lines := strings.Split(string(output), "\n")
|
|
skipHeader := true
|
|
|
|
for _, line := range lines {
|
|
line = strings.TrimSpace(line)
|
|
|
|
// Skip empty lines
|
|
if line == "" {
|
|
continue
|
|
}
|
|
|
|
// Skip header line "Handler"
|
|
if strings.HasPrefix(line, "Handler") {
|
|
skipHeader = false
|
|
continue
|
|
}
|
|
|
|
// Skip separator line "-------------"
|
|
if strings.HasPrefix(line, "---") {
|
|
continue
|
|
}
|
|
|
|
// Skip lines before header (like "Collecting current configuration: done.")
|
|
if skipHeader {
|
|
continue
|
|
}
|
|
|
|
// Skip footer lines
|
|
if strings.Contains(line, "All done") || strings.Contains(line, "Collecting") {
|
|
continue
|
|
}
|
|
|
|
// Map handler names to labels and descriptions
|
|
label, description := s.getHandlerInfo(line)
|
|
handlers = append(handlers, HandlerInfo{
|
|
Name: line,
|
|
Label: label,
|
|
Description: description,
|
|
})
|
|
}
|
|
|
|
return handlers, nil
|
|
}
|
|
|
|
// getHandlerInfo returns a simple label and description for a handler
|
|
func (s *Service) getHandlerInfo(handlerName string) (string, string) {
|
|
handlerInfo := map[string]struct {
|
|
label string
|
|
description string
|
|
}{
|
|
"dev_disk": {
|
|
label: "Physical Disk",
|
|
description: "Physical disk handler",
|
|
},
|
|
"dev_disk_perf": {
|
|
label: "Physical Disk (Performance)",
|
|
description: "Physical disk handler with performance optimizations",
|
|
},
|
|
"dev_cdrom": {
|
|
label: "CDROM",
|
|
description: "CD/DVD-ROM handler",
|
|
},
|
|
"vdisk_blockio": {
|
|
label: "Block Device",
|
|
description: "Virtual disk block I/O handler (for block devices)",
|
|
},
|
|
"vdisk_fileio": {
|
|
label: "Volume",
|
|
description: "Virtual disk file I/O handler (for ZFS volumes and files)",
|
|
},
|
|
"vdisk_nullio": {
|
|
label: "Null Device",
|
|
description: "Null I/O handler (for testing)",
|
|
},
|
|
"vcdrom": {
|
|
label: "CDROM",
|
|
description: "Virtual CD-ROM handler",
|
|
},
|
|
}
|
|
|
|
if info, ok := handlerInfo[handlerName]; ok {
|
|
return info.label, info.description
|
|
}
|
|
|
|
// Default: use handler name as label
|
|
return handlerName, ""
|
|
}
|
|
|
|
// ListPortals lists all iSCSI portals
|
|
func (s *Service) ListPortals(ctx context.Context) ([]Portal, error) {
|
|
query := `
|
|
SELECT id, ip_address, port, is_active, created_at, updated_at
|
|
FROM scst_portals
|
|
ORDER BY ip_address, port
|
|
`
|
|
|
|
rows, err := s.db.QueryContext(ctx, query)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to list portals: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
var portals []Portal
|
|
for rows.Next() {
|
|
var portal Portal
|
|
err := rows.Scan(
|
|
&portal.ID, &portal.IPAddress, &portal.Port,
|
|
&portal.IsActive, &portal.CreatedAt, &portal.UpdatedAt,
|
|
)
|
|
if err != nil {
|
|
s.logger.Error("Failed to scan portal", "error", err)
|
|
continue
|
|
}
|
|
portals = append(portals, portal)
|
|
}
|
|
|
|
return portals, rows.Err()
|
|
}
|
|
|
|
// CreatePortal creates a new iSCSI portal
|
|
func (s *Service) CreatePortal(ctx context.Context, portal *Portal) error {
|
|
// Validate IP address format (basic validation)
|
|
if portal.IPAddress == "" {
|
|
return fmt.Errorf("IP address is required")
|
|
}
|
|
|
|
// Validate port range
|
|
if portal.Port < 1 || portal.Port > 65535 {
|
|
return fmt.Errorf("port must be between 1 and 65535")
|
|
}
|
|
|
|
// Default port to 3260 if not specified
|
|
if portal.Port == 0 {
|
|
portal.Port = 3260
|
|
}
|
|
|
|
// Insert into database
|
|
query := `
|
|
INSERT INTO scst_portals (ip_address, port, is_active)
|
|
VALUES ($1, $2, $3)
|
|
RETURNING id, created_at, updated_at
|
|
`
|
|
|
|
err := s.db.QueryRowContext(ctx, query,
|
|
portal.IPAddress, portal.Port, portal.IsActive,
|
|
).Scan(&portal.ID, &portal.CreatedAt, &portal.UpdatedAt)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create portal: %w", err)
|
|
}
|
|
|
|
s.logger.Info("Portal created", "ip", portal.IPAddress, "port", portal.Port)
|
|
return nil
|
|
}
|
|
|
|
// UpdatePortal updates an existing portal
|
|
func (s *Service) UpdatePortal(ctx context.Context, id string, portal *Portal) error {
|
|
// Validate port range
|
|
if portal.Port < 1 || portal.Port > 65535 {
|
|
return fmt.Errorf("port must be between 1 and 65535")
|
|
}
|
|
|
|
query := `
|
|
UPDATE scst_portals
|
|
SET ip_address = $1, port = $2, is_active = $3, updated_at = NOW()
|
|
WHERE id = $4
|
|
RETURNING updated_at
|
|
`
|
|
|
|
err := s.db.QueryRowContext(ctx, query,
|
|
portal.IPAddress, portal.Port, portal.IsActive, id,
|
|
).Scan(&portal.UpdatedAt)
|
|
if err != nil {
|
|
if err == sql.ErrNoRows {
|
|
return fmt.Errorf("portal not found")
|
|
}
|
|
return fmt.Errorf("failed to update portal: %w", err)
|
|
}
|
|
|
|
s.logger.Info("Portal updated", "id", id, "ip", portal.IPAddress, "port", portal.Port)
|
|
return nil
|
|
}
|
|
|
|
// DeletePortal deletes a portal
|
|
func (s *Service) DeletePortal(ctx context.Context, id string) error {
|
|
result, err := s.db.ExecContext(ctx, "DELETE FROM scst_portals WHERE id = $1", id)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to delete portal: %w", err)
|
|
}
|
|
|
|
rowsAffected, err := result.RowsAffected()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get rows affected: %w", err)
|
|
}
|
|
|
|
if rowsAffected == 0 {
|
|
return fmt.Errorf("portal not found")
|
|
}
|
|
|
|
s.logger.Info("Portal deleted", "id", id)
|
|
return nil
|
|
}
|
|
|
|
// GetPortal retrieves a portal by ID
|
|
func (s *Service) GetPortal(ctx context.Context, id string) (*Portal, error) {
|
|
query := `
|
|
SELECT id, ip_address, port, is_active, created_at, updated_at
|
|
FROM scst_portals
|
|
WHERE id = $1
|
|
`
|
|
|
|
var portal Portal
|
|
err := s.db.QueryRowContext(ctx, query, id).Scan(
|
|
&portal.ID, &portal.IPAddress, &portal.Port,
|
|
&portal.IsActive, &portal.CreatedAt, &portal.UpdatedAt,
|
|
)
|
|
if err != nil {
|
|
if err == sql.ErrNoRows {
|
|
return nil, fmt.Errorf("portal not found")
|
|
}
|
|
return nil, fmt.Errorf("failed to get portal: %w", err)
|
|
}
|
|
|
|
return &portal, nil
|
|
}
|