Add RBAC support with roles, permissions, and session management. Implement middleware for authentication and CSRF protection. Enhance audit logging with additional fields. Update HTTP handlers and routes for new features.
This commit is contained in:
228
internal/service/iscsi/iscsi.go
Normal file
228
internal/service/iscsi/iscsi.go
Normal file
@@ -0,0 +1,228 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user