alpha repo init

This commit is contained in:
2025-12-21 16:25:17 +00:00
parent ad83ae84e4
commit 268af8d691
2308 changed files with 11155 additions and 148 deletions

View File

@@ -8,6 +8,7 @@ import (
"net/url"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"time"
@@ -2046,71 +2047,72 @@ func (a *App) syncSMBSharesFromOS() error {
return nil
}
// syncISCSITargetsFromOS syncs iSCSI targets from targetcli to the store
// syncISCSITargetsFromOS syncs iSCSI targets directly from sysfs (no targetcli needed)
func (a *App) syncISCSITargetsFromOS() error {
log.Printf("debug: starting syncISCSITargetsFromOS")
// Get list of targets from targetcli
// Set TARGETCLI_HOME and TARGETCLI_LOCK_DIR to writable directories
// Create the directories first if they don't exist
os.MkdirAll("/tmp/.targetcli", 0755)
os.MkdirAll("/tmp/targetcli-run", 0755)
// Service runs as root, no need for sudo
cmd := exec.Command("sh", "-c", "TARGETCLI_HOME=/tmp/.targetcli TARGETCLI_LOCK_DIR=/tmp/targetcli-run targetcli /iscsi ls")
output, err := cmd.CombinedOutput()
log.Printf("debug: starting syncISCSITargetsFromOS - reading from sysfs")
// Read iSCSI targets directly from /sys/kernel/config/target/iscsi/
// This avoids targetcli lock file issues
iscsiPath := "/sys/kernel/config/target/iscsi"
entries, err := os.ReadDir(iscsiPath)
if err != nil {
// Log the error but don't fail - targetcli might not be configured
log.Printf("warning: failed to list iSCSI targets from targetcli: %v (output: %s)", err, string(output))
log.Printf("warning: failed to read iSCSI config directory: %v", err)
return nil
}
log.Printf("debug: targetcli output: %s", string(output))
lines := strings.Split(string(output), "\n")
var currentIQN string
for _, line := range lines {
line = strings.TrimSpace(line)
if line == "" {
for _, entry := range entries {
if !entry.IsDir() {
continue
}
// Check if this is a target line (starts with "o- iqn.")
if strings.HasPrefix(line, "o- iqn.") {
log.Printf("debug: found target line: %s", line)
// Extract IQN from line like "o- iqn.2025-12.com.atlas:target-1"
parts := strings.Fields(line)
if len(parts) >= 2 {
currentIQN = parts[1]
// Check if this is an IQN (starts with "iqn.")
iqn := entry.Name()
if !strings.HasPrefix(iqn, "iqn.") {
continue
}
// Check if target already exists in store
existingTargets := a.iscsiStore.List()
exists := false
for _, t := range existingTargets {
if t.IQN == currentIQN {
exists = true
break
}
log.Printf("debug: found iSCSI target: %s", iqn)
// Check if target already exists in store
existingTargets := a.iscsiStore.List()
exists := false
for _, t := range existingTargets {
if t.IQN == iqn {
exists = true
break
}
}
if exists {
log.Printf("debug: target %s already in store, skipping", iqn)
// Still sync LUNs in case they changed
target, err := a.iscsiStore.GetByIQN(iqn)
if err == nil {
if err := a.syncLUNsFromOS(iqn, target.ID, target.Type); err != nil {
log.Printf("warning: failed to sync LUNs for target %s: %v", iqn, err)
}
}
continue
}
if !exists {
// Try to determine target type from IQN
targetType := models.ISCSITargetTypeDisk // Default to disk mode
if strings.Contains(strings.ToLower(currentIQN), "tape") {
targetType = models.ISCSITargetTypeTape
}
// Try to determine target type from IQN
targetType := models.ISCSITargetTypeDisk // Default to disk mode
if strings.Contains(strings.ToLower(iqn), "tape") {
targetType = models.ISCSITargetTypeTape
}
// Create target in store
target, err := a.iscsiStore.CreateWithType(currentIQN, targetType, []string{})
if err != nil && err != storage.ErrISCSITargetExists {
log.Printf("warning: failed to sync iSCSI target %s: %v", currentIQN, err)
} else if err == nil {
log.Printf("synced iSCSI target from OS: %s (type: %s)", currentIQN, targetType)
// Create target in store
target, err := a.iscsiStore.CreateWithType(iqn, targetType, []string{})
if err != nil && err != storage.ErrISCSITargetExists {
log.Printf("warning: failed to sync iSCSI target %s: %v", iqn, err)
continue
} else if err == nil {
log.Printf("synced iSCSI target from OS: %s (type: %s)", iqn, targetType)
// Now try to sync LUNs for this target
if err := a.syncLUNsFromOS(currentIQN, target.ID, targetType); err != nil {
log.Printf("warning: failed to sync LUNs for target %s: %v", currentIQN, err)
}
}
}
// Now try to sync LUNs for this target
if err := a.syncLUNsFromOS(iqn, target.ID, targetType); err != nil {
log.Printf("warning: failed to sync LUNs for target %s: %v", iqn, err)
}
}
}
@@ -2118,119 +2120,163 @@ func (a *App) syncISCSITargetsFromOS() error {
return nil
}
// syncLUNsFromOS syncs LUNs for a specific target from targetcli
// syncLUNsFromOS syncs LUNs for a specific target directly from sysfs (no targetcli needed)
func (a *App) syncLUNsFromOS(iqn, targetID string, targetType models.ISCSITargetType) error {
// Get LUNs for this target
// Service runs as root, no need for sudo
cmd := exec.Command("sh", "-c", "TARGETCLI_HOME=/tmp/.targetcli TARGETCLI_LOCK_DIR=/tmp/targetcli-run targetcli /iscsi/"+iqn+"/tpg1/luns ls")
output, err := cmd.CombinedOutput()
log.Printf("debug: syncing LUNs for target %s from sysfs", iqn)
// Read LUNs directly from /sys/kernel/config/target/iscsi/{iqn}/tpgt_1/lun/
tpgtPath := fmt.Sprintf("/sys/kernel/config/target/iscsi/%s/tpgt_1/lun", iqn)
entries, err := os.ReadDir(tpgtPath)
if err != nil {
// No LUNs or can't read - that's okay, log for debugging
log.Printf("debug: failed to list LUNs for target %s: %v (output: %s)", iqn, err, string(output))
log.Printf("debug: no LUNs directory found for target %s: %v", iqn, err)
return nil // No LUNs is okay
}
// Get target to check existing LUNs
target, err := a.iscsiStore.Get(targetID)
if err != nil {
log.Printf("warning: target %s not found in store", targetID)
return nil
}
lines := strings.Split(string(output), "\n")
for _, line := range lines {
line = strings.TrimSpace(line)
if strings.HasPrefix(line, "o- lun") {
// Parse LUN line like "o- lun0 ....................................... [block/pool-test-02-vol01 (/dev/zvol/pool-test-02/vol01) (default_tg_pt_gp)]"
parts := strings.Fields(line)
if len(parts) >= 2 {
// Extract LUN ID from "lun0"
lunIDStr := strings.TrimPrefix(parts[1], "lun")
lunID, err := strconv.Atoi(lunIDStr)
if err != nil {
continue
}
for _, entry := range entries {
if !entry.IsDir() {
continue
}
// Extract backstore path and device from the line
var backstorePath string
var devicePath string
var zvolName string
// Check if this is a LUN directory (starts with "lun_")
lunDirName := entry.Name()
if !strings.HasPrefix(lunDirName, "lun_") {
continue
}
// Find the part with brackets - might span multiple parts
fullLine := strings.Join(parts, " ")
start := strings.Index(fullLine, "[")
end := strings.LastIndex(fullLine, "]")
if start >= 0 && end > start {
content := fullLine[start+1 : end]
// Parse content like "block/pool-test-02-vol01 (/dev/zvol/pool-test-02/vol01)"
if strings.Contains(content, "(") {
// Has device path
parts2 := strings.Split(content, "(")
if len(parts2) >= 2 {
backstorePath = strings.TrimSpace(parts2[0])
devicePath = strings.Trim(strings.TrimSpace(parts2[1]), "()")
// Extract LUN ID from "lun_0", "lun_1", etc.
lunIDStr := strings.TrimPrefix(lunDirName, "lun_")
lunID, err := strconv.Atoi(lunIDStr)
if err != nil {
log.Printf("debug: invalid LUN directory name: %s", lunDirName)
continue
}
// If device is a zvol, extract ZVOL name
if strings.HasPrefix(devicePath, "/dev/zvol/") {
zvolName = strings.TrimPrefix(devicePath, "/dev/zvol/")
}
}
// Check if LUN already exists
lunExists := false
for _, lun := range target.LUNs {
if lun.ID == lunID {
lunExists = true
break
}
}
if lunExists {
log.Printf("debug: LUN %d already exists for target %s", lunID, iqn)
continue
}
// Find storage_object symlink in LUN directory
// Structure: /sys/kernel/config/target/iscsi/{iqn}/tpgt_1/lun/lun_0/{hash} -> ../../../../../../target/core/iblock_0/{name}
lunPath := fmt.Sprintf("%s/%s", tpgtPath, lunDirName)
storageObjectPath := ""
// Look for symlink in subdirectories (the hash directory that links to backstore)
subEntries, err := os.ReadDir(lunPath)
if err != nil {
log.Printf("debug: failed to read LUN directory %s: %v", lunPath, err)
continue
}
for _, subEntry := range subEntries {
// Check if this is a symlink (the hash directory that links to backstore)
subEntryPath := fmt.Sprintf("%s/%s", lunPath, subEntry.Name())
fileInfo, err := os.Lstat(subEntryPath)
if err != nil {
continue
}
// Check if it's a symlink
if fileInfo.Mode()&os.ModeSymlink != 0 {
if linkTarget, err := os.Readlink(subEntryPath); err == nil {
// Resolve to absolute path
if strings.HasPrefix(linkTarget, "/") {
storageObjectPath = linkTarget
} else {
backstorePath = content
// Relative path, resolve it
absPath, err := filepath.Abs(fmt.Sprintf("%s/%s", lunPath, linkTarget))
if err == nil {
storageObjectPath = absPath
} else {
// Try resolving relative to parent directory
storageObjectPath = filepath.Clean(fmt.Sprintf("%s/%s", filepath.Dir(lunPath), linkTarget))
}
}
break
}
}
}
// Check if LUN already exists
target, err := a.iscsiStore.Get(targetID)
if err != nil {
continue
}
if storageObjectPath == "" {
log.Printf("debug: no storage_object symlink found for LUN %d in target %s", lunID, iqn)
continue
}
lunExists := false
for _, lun := range target.LUNs {
if lun.ID == lunID {
lunExists = true
// Read device path from storage_object/udev_path
udevPathFile := fmt.Sprintf("%s/udev_path", storageObjectPath)
devicePath := ""
if udevPathBytes, err := os.ReadFile(udevPathFile); err == nil {
devicePath = strings.TrimSpace(string(udevPathBytes))
} else {
log.Printf("debug: failed to read udev_path from %s: %v", udevPathFile, err)
}
// Determine backstore type from path
backstoreType := "block"
if strings.Contains(storageObjectPath, "/pscsi/") {
backstoreType = "pscsi"
} else if strings.Contains(storageObjectPath, "/fileio/") {
backstoreType = "fileio"
}
// Extract ZVOL name if device is a zvol
var zvolName string
var size uint64
if strings.HasPrefix(devicePath, "/dev/zvol/") {
zvolName = strings.TrimPrefix(devicePath, "/dev/zvol/")
// Get size from ZFS
zvols, err := a.zfs.ListZVOLs("")
if err == nil {
for _, zvol := range zvols {
if zvol.Name == zvolName {
size = zvol.Size
break
}
}
}
}
if !lunExists {
// Determine backstore type
backstoreType := "block"
if strings.HasPrefix(backstorePath, "pscsi/") {
backstoreType = "pscsi"
} else if strings.HasPrefix(backstorePath, "fileio/") {
backstoreType = "fileio"
}
// Get size if it's a ZVOL
var size uint64
if zvolName != "" {
zvols, err := a.zfs.ListZVOLs("")
if err == nil {
for _, zvol := range zvols {
if zvol.Name == zvolName {
size = zvol.Size
break
}
}
}
}
// Add LUN to store
if targetType == models.ISCSITargetTypeTape && devicePath != "" {
// Tape mode: use device
_, err := a.iscsiStore.AddLUNWithDevice(targetID, "", devicePath, size, backstoreType, "")
if err != nil && err != storage.ErrLUNExists {
log.Printf("warning: failed to sync LUN %d for target %s: %v", lunID, iqn, err)
}
} else if zvolName != "" {
// Disk mode: use ZVOL
_, err := a.iscsiStore.AddLUNWithDevice(targetID, zvolName, "", size, backstoreType, "")
if err != nil && err != storage.ErrLUNExists {
log.Printf("warning: failed to sync LUN %d for target %s: %v", lunID, iqn, err)
}
} else if devicePath != "" {
// Generic device
_, err := a.iscsiStore.AddLUNWithDevice(targetID, "", devicePath, size, backstoreType, "")
if err != nil && err != storage.ErrLUNExists {
log.Printf("warning: failed to sync LUN %d for target %s: %v", lunID, iqn, err)
}
}
}
// Add LUN to store
if targetType == models.ISCSITargetTypeTape && devicePath != "" {
// Tape mode: use device
_, err := a.iscsiStore.AddLUNWithDevice(targetID, "", devicePath, size, backstoreType, "")
if err != nil && err != storage.ErrLUNExists {
log.Printf("warning: failed to sync LUN %d for target %s: %v", lunID, iqn, err)
} else if err == nil {
log.Printf("synced LUN %d from OS for target %s (device: %s)", lunID, iqn, devicePath)
}
} else if zvolName != "" {
// Disk mode: use ZVOL
_, err := a.iscsiStore.AddLUNWithDevice(targetID, zvolName, "", size, backstoreType, "")
if err != nil && err != storage.ErrLUNExists {
log.Printf("warning: failed to sync LUN %d for target %s: %v", lunID, iqn, err)
} else if err == nil {
log.Printf("synced LUN %d from OS for target %s (zvol: %s)", lunID, iqn, zvolName)
}
} else if devicePath != "" {
// Generic device
_, err := a.iscsiStore.AddLUNWithDevice(targetID, "", devicePath, size, backstoreType, "")
if err != nil && err != storage.ErrLUNExists {
log.Printf("warning: failed to sync LUN %d for target %s: %v", lunID, iqn, err)
} else if err == nil {
log.Printf("synced LUN %d from OS for target %s (device: %s)", lunID, iqn, devicePath)
}
}
}

View File

@@ -47,6 +47,7 @@ type App struct {
smbService *services.SMBService
nfsService *services.NFSService
iscsiService *services.ISCSIService
vtlService *services.VTLService
metricsCollector *metrics.Collector
startTime time.Time
backupService *backup.Service
@@ -128,6 +129,7 @@ func New(cfg Config) (*App, error) {
smbService := services.NewSMBService()
nfsService := services.NewNFSService()
iscsiService := services.NewISCSIService()
vtlService := services.NewVTLService()
// Initialize metrics collector
metricsCollector := metrics.NewCollector()
@@ -170,6 +172,7 @@ func New(cfg Config) (*App, error) {
smbService: smbService,
nfsService: nfsService,
iscsiService: iscsiService,
vtlService: vtlService,
metricsCollector: metricsCollector,
startTime: startTime,
backupService: backupService,

View File

@@ -80,6 +80,17 @@ func (a *App) handleManagement(w http.ResponseWriter, r *http.Request) {
a.render(w, "management.html", data)
}
func (a *App) handleVTL(w http.ResponseWriter, r *http.Request) {
data := map[string]any{
"Title": "Virtual Tape Library",
"Build": map[string]string{
"version": "v0.1.0-dev",
},
"ContentTemplate": "vtl-content",
}
a.render(w, "vtl.html", data)
}
func (a *App) handleLoginPage(w http.ResponseWriter, r *http.Request) {
data := map[string]any{
"Title": "Login",

View File

@@ -301,3 +301,25 @@ func (a *App) handleUserOps(w http.ResponseWriter, r *http.Request) {
nil,
)(w, r)
}
// handleVTLDriveOps routes VTL drive operations by method
func (a *App) handleVTLDriveOps(w http.ResponseWriter, r *http.Request) {
methodHandler(
func(w http.ResponseWriter, r *http.Request) { a.handleGetVTLDrive(w, r) },
nil,
nil,
nil,
nil,
)(w, r)
}
// handleVTLTapeOps routes VTL tape operations by method
func (a *App) handleVTLTapeOps(w http.ResponseWriter, r *http.Request) {
methodHandler(
func(w http.ResponseWriter, r *http.Request) { a.handleGetVTLTape(w, r) },
func(w http.ResponseWriter, r *http.Request) { a.handleCreateVTLTape(w, r) },
nil,
func(w http.ResponseWriter, r *http.Request) { a.handleDeleteVTLTape(w, r) },
nil,
)(w, r)
}

View File

@@ -20,6 +20,7 @@ func (a *App) routes() {
a.mux.HandleFunc("/iscsi", a.handleISCSI)
a.mux.HandleFunc("/protection", a.handleProtection)
a.mux.HandleFunc("/management", a.handleManagement)
a.mux.HandleFunc("/vtl", a.handleVTL)
// Health & metrics
a.mux.HandleFunc("/healthz", a.handleHealthz)
@@ -150,6 +151,28 @@ func (a *App) routes() {
))
a.mux.HandleFunc("/api/v1/iscsi/targets/", a.handleISCSITargetOps)
// Storage Services - VTL (Virtual Tape Library)
a.mux.HandleFunc("/api/v1/vtl/status", methodHandler(
func(w http.ResponseWriter, r *http.Request) { a.handleGetVTLStatus(w, r) },
nil, nil, nil, nil,
))
a.mux.HandleFunc("/api/v1/vtl/drives", methodHandler(
func(w http.ResponseWriter, r *http.Request) { a.handleListVTLDrives(w, r) },
nil, nil, nil, nil,
))
a.mux.HandleFunc("/api/v1/vtl/drives/", a.handleVTLDriveOps)
a.mux.HandleFunc("/api/v1/vtl/tapes", methodHandler(
func(w http.ResponseWriter, r *http.Request) { a.handleListVTLTapes(w, r) },
func(w http.ResponseWriter, r *http.Request) { a.handleCreateVTLTape(w, r) },
nil, nil, nil,
))
a.mux.HandleFunc("/api/v1/vtl/tapes/", a.handleVTLTapeOps)
a.mux.HandleFunc("/api/v1/vtl/service", methodHandler(
nil,
func(w http.ResponseWriter, r *http.Request) { a.handleVTLServiceControl(w, r) },
nil, nil, nil,
))
// Job Management
a.mux.HandleFunc("/api/v1/jobs", methodHandler(
func(w http.ResponseWriter, r *http.Request) { a.handleListJobs(w, r) },

View File

@@ -0,0 +1,190 @@
package httpapp
import (
"encoding/json"
"fmt"
"log"
"net/http"
"strconv"
"gitea.avt.data-center.id/othman.suseno/atlas/internal/errors"
)
// VTL API Handlers
// handleGetVTLStatus returns the overall VTL system status
func (a *App) handleGetVTLStatus(w http.ResponseWriter, r *http.Request) {
status, err := a.vtlService.GetStatus()
if err != nil {
log.Printf("get VTL status error: %v", err)
writeError(w, errors.ErrInternal(fmt.Sprintf("failed to get VTL status: %v", err)))
return
}
writeJSON(w, http.StatusOK, status)
}
// handleListVTLDrives returns all virtual tape drives
func (a *App) handleListVTLDrives(w http.ResponseWriter, r *http.Request) {
drives, err := a.vtlService.ListDrives()
if err != nil {
log.Printf("list VTL drives error: %v", err)
writeError(w, errors.ErrInternal(fmt.Sprintf("failed to list VTL drives: %v", err)))
return
}
writeJSON(w, http.StatusOK, drives)
}
// handleListVTLTapes returns all virtual tapes
func (a *App) handleListVTLTapes(w http.ResponseWriter, r *http.Request) {
tapes, err := a.vtlService.ListTapes()
if err != nil {
log.Printf("list VTL tapes error: %v", err)
writeError(w, errors.ErrInternal(fmt.Sprintf("failed to list VTL tapes: %v", err)))
return
}
writeJSON(w, http.StatusOK, tapes)
}
// handleCreateVTLTape creates a new virtual tape
func (a *App) handleCreateVTLTape(w http.ResponseWriter, r *http.Request) {
var req struct {
Barcode string `json:"barcode"`
Type string `json:"type"` // e.g., "LTO-5", "LTO-6"
Size uint64 `json:"size"` // Size in bytes (0 = default)
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, errors.ErrValidation(fmt.Sprintf("invalid request body: %v", err)))
return
}
if req.Barcode == "" {
writeError(w, errors.ErrValidation("barcode is required"))
return
}
if req.Type == "" {
req.Type = "LTO-5" // Default type
}
if err := a.vtlService.CreateTape(req.Barcode, req.Type, req.Size); err != nil {
log.Printf("create VTL tape error: %v", err)
writeError(w, errors.ErrInternal(fmt.Sprintf("failed to create VTL tape: %v", err)))
return
}
writeJSON(w, http.StatusCreated, map[string]string{
"message": "Virtual tape created successfully",
"barcode": req.Barcode,
})
}
// handleDeleteVTLTape deletes a virtual tape
func (a *App) handleDeleteVTLTape(w http.ResponseWriter, r *http.Request) {
barcode := pathParam(r, "barcode")
if barcode == "" {
writeError(w, errors.ErrValidation("barcode is required"))
return
}
if err := a.vtlService.DeleteTape(barcode); err != nil {
log.Printf("delete VTL tape error: %v", err)
writeError(w, errors.ErrInternal(fmt.Sprintf("failed to delete VTL tape: %v", err)))
return
}
writeJSON(w, http.StatusOK, map[string]string{
"message": "Virtual tape deleted successfully",
"barcode": barcode,
})
}
// handleVTLServiceControl controls the mhvtl service (start/stop/restart)
func (a *App) handleVTLServiceControl(w http.ResponseWriter, r *http.Request) {
var req struct {
Action string `json:"action"` // "start", "stop", "restart"
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, errors.ErrValidation(fmt.Sprintf("invalid request body: %v", err)))
return
}
var err error
switch req.Action {
case "start":
err = a.vtlService.StartService()
case "stop":
err = a.vtlService.StopService()
case "restart":
err = a.vtlService.RestartService()
default:
writeError(w, errors.ErrValidation("invalid action: must be 'start', 'stop', or 'restart'"))
return
}
if err != nil {
log.Printf("VTL service control error: %v", err)
writeError(w, errors.ErrInternal(fmt.Sprintf("failed to %s VTL service: %v", req.Action, err)))
return
}
writeJSON(w, http.StatusOK, map[string]string{
"message": "VTL service " + req.Action + "ed successfully",
"action": req.Action,
})
}
// handleGetVTLDrive returns a specific drive by ID
func (a *App) handleGetVTLDrive(w http.ResponseWriter, r *http.Request) {
driveIDStr := pathParam(r, "id")
driveID, err := strconv.Atoi(driveIDStr)
if err != nil {
writeError(w, errors.ErrValidation(fmt.Sprintf("invalid drive ID: %v", err)))
return
}
drives, err := a.vtlService.ListDrives()
if err != nil {
log.Printf("list VTL drives error: %v", err)
writeError(w, errors.ErrInternal(fmt.Sprintf("failed to list VTL drives: %v", err)))
return
}
for _, drive := range drives {
if drive.ID == driveID {
writeJSON(w, http.StatusOK, drive)
return
}
}
writeError(w, errors.ErrNotFound("drive not found"))
}
// handleGetVTLTape returns a specific tape by barcode
func (a *App) handleGetVTLTape(w http.ResponseWriter, r *http.Request) {
barcode := pathParam(r, "barcode")
if barcode == "" {
writeError(w, errors.ErrValidation("barcode is required"))
return
}
tapes, err := a.vtlService.ListTapes()
if err != nil {
log.Printf("list VTL tapes error: %v", err)
writeError(w, errors.ErrInternal(fmt.Sprintf("failed to list VTL tapes: %v", err)))
return
}
for _, tape := range tapes {
if tape.Barcode == barcode {
writeJSON(w, http.StatusOK, tape)
return
}
}
writeError(w, errors.ErrNotFound("tape not found"))
}