alpha repo init
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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) },
|
||||
|
||||
190
internal/httpapp/vtl_handlers.go
Normal file
190
internal/httpapp/vtl_handlers.go
Normal 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"))
|
||||
}
|
||||
58
internal/models/vtl.go
Normal file
58
internal/models/vtl.go
Normal file
@@ -0,0 +1,58 @@
|
||||
package models
|
||||
|
||||
// VTLDrive represents a virtual tape drive
|
||||
type VTLDrive struct {
|
||||
ID int `json:"id"` // Drive ID (e.g., 11, 12, 13, 14 for library 10)
|
||||
LibraryID int `json:"library_id"` // Library ID (tens digit)
|
||||
SlotID int `json:"slot_id"` // Slot ID (ones digit)
|
||||
Vendor string `json:"vendor"` // Drive vendor (e.g., "IBM")
|
||||
Product string `json:"product"` // Drive product (e.g., "ULT3580-TD5")
|
||||
Type string `json:"type"` // Tape type (e.g., "LTO-5", "LTO-6")
|
||||
Device string `json:"device"` // Device path (e.g., "/dev/st0")
|
||||
Status string `json:"status"` // "online", "offline", "error"
|
||||
MediaLoaded bool `json:"media_loaded"` // Whether tape is loaded
|
||||
Barcode string `json:"barcode"` // Barcode of loaded tape (if any)
|
||||
}
|
||||
|
||||
// VTLMediaChanger represents a virtual media changer
|
||||
type VTLMediaChanger struct {
|
||||
ID int `json:"id"` // Changer ID
|
||||
LibraryID int `json:"library_id"` // Library ID
|
||||
Device string `json:"device"` // Device path (e.g., "/dev/sg0")
|
||||
Status string `json:"status"` // "online", "offline", "error"
|
||||
Slots int `json:"slots"` // Number of slots
|
||||
Drives int `json:"drives"` // Number of drives
|
||||
}
|
||||
|
||||
// VTLTape represents a virtual tape cartridge
|
||||
type VTLTape struct {
|
||||
Barcode string `json:"barcode"` // Tape barcode
|
||||
LibraryID int `json:"library_id"` // Library ID
|
||||
SlotID int `json:"slot_id"` // Slot ID (0 = not in library)
|
||||
DriveID int `json:"drive_id"` // Drive ID if loaded (-1 if not loaded)
|
||||
Type string `json:"type"` // Tape type (e.g., "LTO-5")
|
||||
Size uint64 `json:"size"` // Tape capacity in bytes
|
||||
Used uint64 `json:"used"` // Used space in bytes
|
||||
Status string `json:"status"` // "available", "in_use", "error"
|
||||
}
|
||||
|
||||
// VTLConfig represents mhvtl configuration
|
||||
type VTLConfig struct {
|
||||
Enabled bool `json:"enabled"` // Whether VTL is enabled
|
||||
LibraryID int `json:"library_id"` // Default library ID
|
||||
Drives []VTLDrive `json:"drives"` // List of drives
|
||||
Changer *VTLMediaChanger `json:"changer"` // Media changer
|
||||
Tapes []VTLTape `json:"tapes"` // List of tapes
|
||||
ConfigPath string `json:"config_path"` // Path to mhvtl config
|
||||
StoragePath string `json:"storage_path"` // Path to tape storage
|
||||
}
|
||||
|
||||
// VTLStatus represents overall VTL system status
|
||||
type VTLStatus struct {
|
||||
ServiceRunning bool `json:"service_running"` // Whether mhvtl service is running
|
||||
DrivesOnline int `json:"drives_online"` // Number of online drives
|
||||
DrivesTotal int `json:"drives_total"` // Total number of drives
|
||||
TapesTotal int `json:"tapes_total"` // Total number of tapes
|
||||
TapesAvailable int `json:"tapes_available"` // Number of available tapes
|
||||
LastError string `json:"last_error"` // Last error message (if any)
|
||||
}
|
||||
295
internal/services/vtl.go
Normal file
295
internal/services/vtl.go
Normal file
@@ -0,0 +1,295 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"gitea.avt.data-center.id/othman.suseno/atlas/internal/models"
|
||||
)
|
||||
|
||||
// VTLService manages mhvtl (Virtual Tape Library)
|
||||
type VTLService struct {
|
||||
configPath string // Path to mhvtl config (default: /etc/mhvtl/mhvtl.conf)
|
||||
storagePath string // Path to tape storage (default: /opt/mhvtl)
|
||||
}
|
||||
|
||||
// NewVTLService creates a new VTL service
|
||||
func NewVTLService() *VTLService {
|
||||
return &VTLService{
|
||||
configPath: "/etc/mhvtl/mhvtl.conf",
|
||||
storagePath: "/opt/mhvtl",
|
||||
}
|
||||
}
|
||||
|
||||
// GetStatus returns the status of mhvtl service
|
||||
func (s *VTLService) GetStatus() (*models.VTLStatus, error) {
|
||||
status := &models.VTLStatus{
|
||||
ServiceRunning: false,
|
||||
DrivesOnline: 0,
|
||||
DrivesTotal: 0,
|
||||
TapesTotal: 0,
|
||||
TapesAvailable: 0,
|
||||
}
|
||||
|
||||
// Check if mhvtl service is running
|
||||
cmd := exec.Command("systemctl", "is-active", "mhvtl")
|
||||
if err := cmd.Run(); err == nil {
|
||||
status.ServiceRunning = true
|
||||
}
|
||||
|
||||
// Get drives and tapes info
|
||||
drives, err := s.ListDrives()
|
||||
if err == nil {
|
||||
status.DrivesTotal = len(drives)
|
||||
for _, drive := range drives {
|
||||
if drive.Status == "online" {
|
||||
status.DrivesOnline++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tapes, err := s.ListTapes()
|
||||
if err == nil {
|
||||
status.TapesTotal = len(tapes)
|
||||
for _, tape := range tapes {
|
||||
if tape.Status == "available" {
|
||||
status.TapesAvailable++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return status, nil
|
||||
}
|
||||
|
||||
// ListDrives lists all virtual tape drives
|
||||
func (s *VTLService) ListDrives() ([]models.VTLDrive, error) {
|
||||
drives := []models.VTLDrive{}
|
||||
|
||||
// Read from /sys/class/scsi_tape/
|
||||
tapePath := "/sys/class/scsi_tape"
|
||||
entries, err := os.ReadDir(tapePath)
|
||||
if err != nil {
|
||||
// mhvtl might not be installed or configured
|
||||
return drives, nil
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
if !entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
|
||||
// Entry name format: st0, st1, etc.
|
||||
deviceName := entry.Name()
|
||||
if !strings.HasPrefix(deviceName, "st") && !strings.HasPrefix(deviceName, "nst") {
|
||||
continue
|
||||
}
|
||||
|
||||
// Get device path
|
||||
devicePath := fmt.Sprintf("/dev/%s", deviceName)
|
||||
|
||||
// Try to get vendor and product from sysfs
|
||||
vendorPath := fmt.Sprintf("%s/%s/device/vendor", tapePath, deviceName)
|
||||
productPath := fmt.Sprintf("%s/%s/device/model", tapePath, deviceName)
|
||||
|
||||
vendor := "Unknown"
|
||||
product := "Unknown"
|
||||
if vendorBytes, err := os.ReadFile(vendorPath); err == nil {
|
||||
vendor = strings.TrimSpace(string(vendorBytes))
|
||||
}
|
||||
if productBytes, err := os.ReadFile(productPath); err == nil {
|
||||
product = strings.TrimSpace(string(productBytes))
|
||||
}
|
||||
|
||||
// Determine drive type from product
|
||||
driveType := s.determineDriveType(product)
|
||||
|
||||
// Try to get drive ID from mhvtl config or device
|
||||
driveID := s.getDriveIDFromDevice(deviceName)
|
||||
|
||||
// Check if media is loaded
|
||||
mediaLoaded, barcode := s.checkMediaLoaded(deviceName)
|
||||
|
||||
drive := models.VTLDrive{
|
||||
ID: driveID,
|
||||
LibraryID: driveID / 10, // Extract library ID (tens digit)
|
||||
SlotID: driveID % 10, // Extract slot ID (ones digit)
|
||||
Vendor: vendor,
|
||||
Product: product,
|
||||
Type: driveType,
|
||||
Device: devicePath,
|
||||
Status: "online",
|
||||
MediaLoaded: mediaLoaded,
|
||||
Barcode: barcode,
|
||||
}
|
||||
|
||||
drives = append(drives, drive)
|
||||
}
|
||||
|
||||
return drives, nil
|
||||
}
|
||||
|
||||
// ListTapes lists all virtual tapes
|
||||
func (s *VTLService) ListTapes() ([]models.VTLTape, error) {
|
||||
tapes := []models.VTLTape{}
|
||||
|
||||
// Read tapes from storage directory
|
||||
tapeStoragePath := filepath.Join(s.storagePath, "data")
|
||||
entries, err := os.ReadDir(tapeStoragePath)
|
||||
if err != nil {
|
||||
// No tapes directory yet
|
||||
return tapes, nil
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
// Directory name is usually the barcode
|
||||
barcode := entry.Name()
|
||||
tapePath := filepath.Join(tapeStoragePath, barcode)
|
||||
|
||||
// Get tape info
|
||||
tape := models.VTLTape{
|
||||
Barcode: barcode,
|
||||
LibraryID: 10, // Default library ID
|
||||
SlotID: 0, // Not in library by default
|
||||
DriveID: -1, // Not loaded
|
||||
Type: "LTO-5", // Default type
|
||||
Size: 0,
|
||||
Used: 0,
|
||||
Status: "available",
|
||||
}
|
||||
|
||||
// Try to get size from tape file
|
||||
tapeFile := filepath.Join(tapePath, "tape")
|
||||
if info, err := os.Stat(tapeFile); err == nil {
|
||||
tape.Size = uint64(info.Size())
|
||||
tape.Used = uint64(info.Size()) // For now, assume used = size
|
||||
}
|
||||
|
||||
tapes = append(tapes, tape)
|
||||
}
|
||||
}
|
||||
|
||||
return tapes, nil
|
||||
}
|
||||
|
||||
// CreateTape creates a new virtual tape
|
||||
func (s *VTLService) CreateTape(barcode string, tapeType string, size uint64) error {
|
||||
// Create tape directory
|
||||
tapePath := filepath.Join(s.storagePath, "data", barcode)
|
||||
if err := os.MkdirAll(tapePath, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create tape directory: %v", err)
|
||||
}
|
||||
|
||||
// Create tape file
|
||||
tapeFile := filepath.Join(tapePath, "tape")
|
||||
file, err := os.Create(tapeFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create tape file: %v", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// Pre-allocate space if size is specified
|
||||
if size > 0 {
|
||||
if err := file.Truncate(int64(size)); err != nil {
|
||||
return fmt.Errorf("failed to pre-allocate tape space: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("Created virtual tape: %s (type: %s, size: %d bytes)", barcode, tapeType, size)
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteTape deletes a virtual tape
|
||||
func (s *VTLService) DeleteTape(barcode string) error {
|
||||
tapePath := filepath.Join(s.storagePath, "data", barcode)
|
||||
if err := os.RemoveAll(tapePath); err != nil {
|
||||
return fmt.Errorf("failed to delete tape: %v", err)
|
||||
}
|
||||
|
||||
log.Printf("Deleted virtual tape: %s", barcode)
|
||||
return nil
|
||||
}
|
||||
|
||||
// StartService starts the mhvtl service
|
||||
func (s *VTLService) StartService() error {
|
||||
cmd := exec.Command("systemctl", "start", "mhvtl")
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to start mhvtl service: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// StopService stops the mhvtl service
|
||||
func (s *VTLService) StopService() error {
|
||||
cmd := exec.Command("systemctl", "stop", "mhvtl")
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to stop mhvtl service: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// RestartService restarts the mhvtl service
|
||||
func (s *VTLService) RestartService() error {
|
||||
cmd := exec.Command("systemctl", "restart", "mhvtl")
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to restart mhvtl service: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
|
||||
func (s *VTLService) determineDriveType(product string) string {
|
||||
product = strings.ToUpper(product)
|
||||
if strings.Contains(product, "LTO-9") || strings.Contains(product, "TD9") {
|
||||
return "LTO-9"
|
||||
}
|
||||
if strings.Contains(product, "LTO-8") || strings.Contains(product, "TD8") {
|
||||
return "LTO-8"
|
||||
}
|
||||
if strings.Contains(product, "LTO-7") || strings.Contains(product, "TD7") {
|
||||
return "LTO-7"
|
||||
}
|
||||
if strings.Contains(product, "LTO-6") || strings.Contains(product, "TD6") {
|
||||
return "LTO-6"
|
||||
}
|
||||
if strings.Contains(product, "LTO-5") || strings.Contains(product, "TD5") {
|
||||
return "LTO-5"
|
||||
}
|
||||
return "Unknown"
|
||||
}
|
||||
|
||||
func (s *VTLService) getDriveIDFromDevice(deviceName string) int {
|
||||
// Extract number from device name (e.g., "st0" -> 0, "nst1" -> 1)
|
||||
// For now, use a simple mapping
|
||||
// In real implementation, this should read from mhvtl config
|
||||
if strings.HasPrefix(deviceName, "nst") {
|
||||
numStr := strings.TrimPrefix(deviceName, "nst")
|
||||
if num, err := strconv.Atoi(numStr); err == nil {
|
||||
return num + 10 // Assume library 10
|
||||
}
|
||||
} else if strings.HasPrefix(deviceName, "st") {
|
||||
numStr := strings.TrimPrefix(deviceName, "st")
|
||||
if num, err := strconv.Atoi(numStr); err == nil {
|
||||
return num + 10 // Assume library 10
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (s *VTLService) checkMediaLoaded(deviceName string) (bool, string) {
|
||||
// Check if tape is loaded by reading from device
|
||||
// This is a simplified check - in real implementation, use mt or sg commands
|
||||
statusPath := fmt.Sprintf("/sys/class/scsi_tape/%s/device/tape_stat", deviceName)
|
||||
if _, err := os.Stat(statusPath); err == nil {
|
||||
// Tape device exists, might have media
|
||||
// For now, return false - real implementation should check actual status
|
||||
return false, ""
|
||||
}
|
||||
return false, ""
|
||||
}
|
||||
Reference in New Issue
Block a user