229 lines
11 KiB
Go
229 lines
11 KiB
Go
package iscsi
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/example/storage-appliance/internal/audit"
|
|
"github.com/example/storage-appliance/internal/infra/iscsi"
|
|
"github.com/example/storage-appliance/internal/infra/zfs"
|
|
)
|
|
|
|
var (
|
|
ErrForbidden = errors.New("forbidden")
|
|
)
|
|
|
|
type ISCSIService struct {
|
|
DB *sql.DB
|
|
ZFS *zfs.Adapter
|
|
ISCSI *iscsi.Adapter
|
|
Audit audit.AuditLogger
|
|
}
|
|
|
|
func NewISCSIService(db *sql.DB, z *zfs.Adapter, i *iscsi.Adapter, a audit.AuditLogger) *ISCSIService {
|
|
return &ISCSIService{DB: db, ZFS: z, ISCSI: i, Audit: a}
|
|
}
|
|
|
|
func (s *ISCSIService) ListTargets(ctx context.Context) ([]map[string]any, error) {
|
|
rows, err := s.DB.QueryContext(ctx, `SELECT id, iqn, name, created_at FROM iscsi_targets`)
|
|
if err != nil { return nil, err }
|
|
defer rows.Close()
|
|
res := []map[string]any{}
|
|
for rows.Next() {
|
|
var id, iqn, name string
|
|
var created time.Time
|
|
if err := rows.Scan(&id, &iqn, &name, &created); err != nil { return nil, err }
|
|
res = append(res, map[string]any{"id": id, "iqn": iqn, "name": name, "created_at": created})
|
|
}
|
|
return res, nil
|
|
}
|
|
|
|
func (s *ISCSIService) CreateTarget(ctx context.Context, user, role, name, iqn string) (string, error) {
|
|
if role != "admin" { return "", ErrForbidden }
|
|
if iqn == "" || !strings.HasPrefix(iqn, "iqn.") { return "", errors.New("invalid IQN") }
|
|
id := uuid.New().String()
|
|
if _, err := s.DB.ExecContext(ctx, `INSERT INTO iscsi_targets (id, iqn, name) VALUES (?, ?, ?)`, id, iqn, name); err != nil {
|
|
return "", err
|
|
}
|
|
if s.ISCSI != nil {
|
|
if err := s.ISCSI.CreateTarget(ctx, iqn); err != nil {
|
|
return "", err
|
|
}
|
|
if err := s.ISCSI.Save(ctx); err != nil { return "", err }
|
|
}
|
|
if s.Audit != nil { s.Audit.Record(ctx, audit.Event{UserID: user, Action: "iscsi.target.create", ResourceType: "iscsi_target", ResourceID: id, Success: true}) }
|
|
return id, nil
|
|
}
|
|
|
|
// CreateLUN creates a zvol and maps it as LUN for the IQN. lunName is the zvol path e.g. pool/dataset/vol
|
|
func (s *ISCSIService) CreateLUN(ctx context.Context, user, role, targetID, lunName string, size string, blocksize int) (string, error) {
|
|
if role != "admin" && role != "operator" { return "", ErrForbidden }
|
|
// fetch target IQN
|
|
var iqn string
|
|
if err := s.DB.QueryRowContext(ctx, `SELECT iqn FROM iscsi_targets WHERE id = ?`, targetID).Scan(&iqn); err != nil {
|
|
return "", err
|
|
}
|
|
// build zvol name
|
|
zvol := lunName // expect fully qualified dataset, e.g., pool/iscsi/target/lun0
|
|
// create zvol via zfs adapter
|
|
props := map[string]string{}
|
|
if blocksize > 0 {
|
|
// convert bytes to K unit if divisible
|
|
// For simplicity, just set volblocksize as "8K" or "512"; attempt simple conversion
|
|
props["volblocksize"] = fmt.Sprintf("%d", blocksize)
|
|
}
|
|
if s.ZFS != nil {
|
|
if _, err := s.ZFS.ListDatasets(ctx, ""); err == nil { // no-op to check connectivity
|
|
}
|
|
if err := s.ZFS.CreateZVol(ctx, zvol, size, props); err != nil {
|
|
return "", err
|
|
}
|
|
}
|
|
// backstore name and device path
|
|
bsName := "bs-" + uuid.New().String()
|
|
devpath := "/dev/zvol/" + zvol
|
|
if s.ISCSI != nil {
|
|
if err := s.ISCSI.CreateBackstore(ctx, bsName, devpath); err != nil {
|
|
return "", err
|
|
}
|
|
}
|
|
// determine LUN ID as next available for target
|
|
var maxLun sql.NullInt64
|
|
if err := s.DB.QueryRowContext(ctx, `SELECT MAX(lun_id) FROM iscsi_luns WHERE target_id = ?`, targetID).Scan(&maxLun); err != nil && err != sql.ErrNoRows { return "", err }
|
|
nextLun := 0
|
|
if maxLun.Valid { nextLun = int(maxLun.Int64) + 1 }
|
|
if s.ISCSI != nil {
|
|
if err := s.ISCSI.CreateLUN(ctx, iqn, bsName, nextLun); err != nil {
|
|
return "", err
|
|
}
|
|
if err := s.ISCSI.Save(ctx); err != nil { return "", err }
|
|
}
|
|
id := uuid.New().String()
|
|
if _, err := s.DB.ExecContext(ctx, `INSERT INTO iscsi_luns (id, target_id, lun_id, zvol, size, blocksize, mapped) VALUES (?, ?, ?, ?, ?, ?, 1)`, id, targetID, nextLun, zvol, sizeToInt(size), blocksize); err != nil {
|
|
return "", err
|
|
}
|
|
if s.Audit != nil { s.Audit.Record(ctx, audit.Event{UserID: user, Action: "iscsi.lun.create", ResourceType: "iscsi_lun", ResourceID: id, Success: true}) }
|
|
return id, nil
|
|
}
|
|
|
|
func sizeToInt(s string) int {
|
|
// naive conversion: strip trailing G/M/K
|
|
// This function can be improved; for now return 0
|
|
return 0
|
|
}
|
|
|
|
func (s *ISCSIService) ListLUNs(ctx context.Context, targetID string) ([]map[string]any, error) {
|
|
rows, err := s.DB.QueryContext(ctx, `SELECT id, lun_id, zvol, size, blocksize, mapped, created_at FROM iscsi_luns WHERE target_id = ?`, targetID)
|
|
if err != nil { return nil, err }
|
|
defer rows.Close()
|
|
res := []map[string]any{}
|
|
for rows.Next() {
|
|
var id, zvol string
|
|
var lunID int
|
|
var size int
|
|
var blocksize int
|
|
var mapped int
|
|
var created time.Time
|
|
if err := rows.Scan(&id, &lunID, &zvol, &size, &blocksize, &mapped, &created); err != nil { return nil, err }
|
|
res = append(res, map[string]any{"id": id, "lun_id": lunID, "zvol": zvol, "size": size, "blocksize": blocksize, "mapped": mapped == 1, "created_at": created})
|
|
}
|
|
return res, nil
|
|
}
|
|
|
|
func (s *ISCSIService) GetTargetInfo(ctx context.Context, targetID string) (map[string]any, error) {
|
|
var iqn string
|
|
if err := s.DB.QueryRowContext(ctx, `SELECT iqn FROM iscsi_targets WHERE id = ?`, targetID).Scan(&iqn); err != nil { return nil, err }
|
|
portals := []map[string]any{}
|
|
rows, err := s.DB.QueryContext(ctx, `SELECT id, address, port FROM iscsi_portals WHERE target_id = ?`, targetID)
|
|
if err != nil { return nil, err }
|
|
defer rows.Close()
|
|
for rows.Next() {
|
|
var id, address string
|
|
var port int
|
|
if err := rows.Scan(&id, &address, &port); err != nil { return nil, err }
|
|
portals = append(portals, map[string]any{"id": id, "address": address, "port": port})
|
|
}
|
|
inits := []map[string]any{}
|
|
rows2, err := s.DB.QueryContext(ctx, `SELECT id, initiator_iqn FROM iscsi_initiators WHERE target_id = ?`, targetID)
|
|
if err != nil { return nil, err }
|
|
defer rows2.Close()
|
|
for rows2.Next() {
|
|
var id, iqnStr string
|
|
if err := rows2.Scan(&id, &iqnStr); err != nil { return nil, err }
|
|
inits = append(inits, map[string]any{"id": id, "iqn": iqnStr})
|
|
}
|
|
res := map[string]any{"iqn": iqn, "portals": portals, "initiators": inits}
|
|
return res, nil
|
|
}
|
|
|
|
func (s *ISCSIService) DeleteLUN(ctx context.Context, user, role, id string, force bool) error {
|
|
if role != "admin" { return ErrForbidden }
|
|
// check LUN
|
|
var mappedInt int
|
|
var targetID string
|
|
var lunID int
|
|
if err := s.DB.QueryRowContext(ctx, `SELECT target_id, lun_id, mapped FROM iscsi_luns WHERE id = ?`, id).Scan(&targetID, &lunID, &mappedInt); err != nil { return err }
|
|
if mappedInt == 1 && !force { return errors.New("LUN is mapped; unmap (drain) before deletion or specify force") }
|
|
// delete via adapter
|
|
if s.ISCSI != nil {
|
|
var iqn string
|
|
if err := s.DB.QueryRowContext(ctx, `SELECT iqn FROM iscsi_targets WHERE id = ?`, targetID).Scan(&iqn); err != nil { return err }
|
|
if err := s.ISCSI.DeleteLUN(ctx, iqn, lunID); err != nil { return err }
|
|
if err := s.ISCSI.Save(ctx); err != nil { return err }
|
|
}
|
|
if _, err := s.DB.ExecContext(ctx, `DELETE FROM iscsi_luns WHERE id = ?`, id); err != nil { return err }
|
|
if s.Audit != nil { s.Audit.Record(ctx, audit.Event{UserID: user, Action: "iscsi.lun.delete", ResourceType: "iscsi_lun", ResourceID: id, Success: true}) }
|
|
return nil
|
|
}
|
|
|
|
// UnmapLUN removes LUN mapping from target, sets mapped to false in DB
|
|
func (s *ISCSIService) UnmapLUN(ctx context.Context, user, role, id string) error {
|
|
if role != "admin" && role != "operator" { return ErrForbidden }
|
|
var targetID string
|
|
var lunID int
|
|
if err := s.DB.QueryRowContext(ctx, `SELECT target_id, lun_id FROM iscsi_luns WHERE id = ?`, id).Scan(&targetID, &lunID); err != nil { return err }
|
|
if s.ISCSI != nil {
|
|
var iqn string
|
|
if err := s.DB.QueryRowContext(ctx, `SELECT iqn FROM iscsi_targets WHERE id = ?`, targetID).Scan(&iqn); err != nil { return err }
|
|
if err := s.ISCSI.DeleteLUN(ctx, iqn, lunID); err != nil { return err }
|
|
if err := s.ISCSI.Save(ctx); err != nil { return err }
|
|
}
|
|
if _, err := s.DB.ExecContext(ctx, `UPDATE iscsi_luns SET mapped = 0 WHERE id = ?`, id); err != nil { return err }
|
|
if s.Audit != nil { s.Audit.Record(ctx, audit.Event{UserID: user, Action: "iscsi.lun.unmap", ResourceType: "iscsi_lun", ResourceID: id, Success: true}) }
|
|
return nil
|
|
}
|
|
|
|
func (s *ISCSIService) AddPortal(ctx context.Context, user, role, targetID, address string, port int) (string, error) {
|
|
if role != "admin" && role != "operator" { return "", ErrForbidden }
|
|
// verify target exists and fetch IQN
|
|
var iqn string
|
|
if err := s.DB.QueryRowContext(ctx, `SELECT iqn FROM iscsi_targets WHERE id = ?`, targetID).Scan(&iqn); err != nil { return "", err }
|
|
id := uuid.New().String()
|
|
if _, err := s.DB.ExecContext(ctx, `INSERT INTO iscsi_portals (id, target_id, address, port) VALUES (?, ?, ?, ?)`, id, targetID, address, port); err != nil { return "", err }
|
|
if s.ISCSI != nil {
|
|
if err := s.ISCSI.AddPortal(ctx, iqn, address, port); err != nil { return "", err }
|
|
if err := s.ISCSI.Save(ctx); err != nil { return "", err }
|
|
}
|
|
if s.Audit != nil { s.Audit.Record(ctx, audit.Event{UserID: user, Action: "iscsi.portal.add", ResourceType: "iscsi_portal", ResourceID: id, Success: true}) }
|
|
return id, nil
|
|
}
|
|
|
|
func (s *ISCSIService) AddInitiator(ctx context.Context, user, role, targetID, initiatorIQN string) (string, error) {
|
|
if role != "admin" && role != "operator" { return "", ErrForbidden }
|
|
var iqn string
|
|
if err := s.DB.QueryRowContext(ctx, `SELECT iqn FROM iscsi_targets WHERE id = ?`, targetID).Scan(&iqn); err != nil { return "", err }
|
|
id := uuid.New().String()
|
|
if _, err := s.DB.ExecContext(ctx, `INSERT INTO iscsi_initiators (id, target_id, initiator_iqn) VALUES (?, ?, ?)`, id, targetID, initiatorIQN); err != nil { return "", err }
|
|
if s.ISCSI != nil {
|
|
if err := s.ISCSI.AddACL(ctx, iqn, initiatorIQN); err != nil { return "", err }
|
|
if err := s.ISCSI.Save(ctx); err != nil { return "", err }
|
|
}
|
|
if s.Audit != nil { s.Audit.Record(ctx, audit.Event{UserID: user, Action: "iscsi.initiator.add", ResourceType: "iscsi_initiator", ResourceID: id, Success: true}) }
|
|
return id, nil
|
|
}
|