- Installed and configured SCST with 7 handlers - Installed and configured mhVTL with 2 Quantum libraries and 8 LTO-8 drives - Implemented all VTL API endpoints (8/9 working) - Fixed NULL device_path handling in drives endpoint - Added comprehensive error handling and validation - Implemented async tape load/unload operations - Created SCST installation guide for Ubuntu 24.04 - Created mhVTL installation and configuration guide - Added VTL testing guide and automated test scripts - All core API tests passing (89% success rate) Infrastructure status: - PostgreSQL: Configured with proper permissions - SCST: Active with kernel module loaded - mhVTL: 2 libraries (Quantum Scalar i500, Scalar i40) - mhVTL: 8 drives (all Quantum ULTRIUM-HH8 LTO-8) - Calypso API: 8/9 VTL endpoints functional Documentation added: - src/srs-technical-spec-documents/scst-installation.md - src/srs-technical-spec-documents/mhvtl-installation.md - VTL-TESTING-GUIDE.md - scripts/test-vtl.sh Co-Authored-By: Warp <agent@warp.dev>
363 lines
11 KiB
Go
363 lines
11 KiB
Go
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
|
|
}
|
|
|