package scst import ( "context" "database/sql" "fmt" "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"` 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"` 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"` } // 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() if err != nil { // Check if target already exists if strings.Contains(string(output), "already exists") { s.logger.Warn("Target already exists in SCST", "iqn", target.IQN) } else { return fmt.Errorf("failed to create SCST target: %s: %w", string(output), err) } } // 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 } // ListTargets lists all SCST targets func (s *Service) ListTargets(ctx context.Context) ([]Target, error) { query := ` SELECT id, iqn, target_type, name, description, is_active, single_initiator_only, created_at, updated_at, created_by FROM scst_targets ORDER BY 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 err := rows.Scan( &target.ID, &target.IQN, &target.TargetType, &target.Name, &target.Description, &target.IsActive, &target.SingleInitiatorOnly, &target.CreatedAt, &target.UpdatedAt, &target.CreatedBy, ) if err != nil { s.logger.Error("Failed to scan target", "error", err) continue } 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 err := s.db.QueryRowContext(ctx, query, id).Scan( &target.ID, &target.IQN, &target.TargetType, &target.Name, &target.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) } return &target, nil } // GetTargetLUNs retrieves all LUNs for a target func (s *Service) GetTargetLUNs(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 } luns = append(luns, lun) } return luns, rows.Err() } // 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 } // DetectHandlers detects available SCST handlers func (s *Service) DetectHandlers(ctx context.Context) ([]string, 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 (simplified - actual parsing would be more robust) handlers := []string{} lines := strings.Split(string(output), "\n") for _, line := range lines { line = strings.TrimSpace(line) if line != "" && !strings.HasPrefix(line, "Handler") { handlers = append(handlers, line) } } return handlers, nil }