50%
This commit is contained in:
@@ -1,11 +1,14 @@
|
||||
package httpapp
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"gitea.avt.data-center.id/othman.suseno/atlas/internal/errors"
|
||||
"gitea.avt.data-center.id/othman.suseno/atlas/internal/models"
|
||||
)
|
||||
|
||||
// methodHandler routes requests based on HTTP method
|
||||
@@ -85,8 +88,9 @@ func (a *App) handlePoolOps(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
if strings.HasSuffix(r.URL.Path, "/scrub") {
|
||||
storageRoles := []models.Role{models.RoleAdministrator, models.RoleOperator}
|
||||
if r.Method == http.MethodPost {
|
||||
a.handleScrubPool(w, r)
|
||||
a.requireRole(storageRoles...)(http.HandlerFunc(a.handleScrubPool)).ServeHTTP(w, r)
|
||||
} else if r.Method == http.MethodGet {
|
||||
a.handleGetScrubStatus(w, r)
|
||||
} else {
|
||||
@@ -96,8 +100,9 @@ func (a *App) handlePoolOps(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
if strings.HasSuffix(r.URL.Path, "/export") {
|
||||
storageRoles := []models.Role{models.RoleAdministrator, models.RoleOperator}
|
||||
if r.Method == http.MethodPost {
|
||||
a.handleExportPool(w, r)
|
||||
a.requireRole(storageRoles...)(http.HandlerFunc(a.handleExportPool)).ServeHTTP(w, r)
|
||||
} else {
|
||||
writeError(w, errors.NewAPIError(errors.ErrCodeBadRequest, "method not allowed", http.StatusMethodNotAllowed))
|
||||
}
|
||||
@@ -106,50 +111,67 @@ func (a *App) handlePoolOps(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
if strings.HasSuffix(r.URL.Path, "/spare") {
|
||||
if r.Method == http.MethodPost {
|
||||
a.handleAddSpareDisk(w, r)
|
||||
storageRoles := []models.Role{models.RoleAdministrator, models.RoleOperator}
|
||||
a.requireRole(storageRoles...)(http.HandlerFunc(a.handleAddSpareDisk)).ServeHTTP(w, r)
|
||||
} else {
|
||||
writeError(w, errors.NewAPIError(errors.ErrCodeBadRequest, "method not allowed", http.StatusMethodNotAllowed))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
storageRoles := []models.Role{models.RoleAdministrator, models.RoleOperator}
|
||||
methodHandler(
|
||||
func(w http.ResponseWriter, r *http.Request) { a.handleGetPool(w, r) },
|
||||
nil,
|
||||
nil,
|
||||
func(w http.ResponseWriter, r *http.Request) { a.handleDeletePool(w, r) },
|
||||
func(w http.ResponseWriter, r *http.Request) {
|
||||
a.requireRole(storageRoles...)(http.HandlerFunc(a.handleDeletePool)).ServeHTTP(w, r)
|
||||
},
|
||||
nil,
|
||||
)(w, r)
|
||||
}
|
||||
|
||||
// handleDatasetOps routes dataset operations by method
|
||||
func (a *App) handleDatasetOps(w http.ResponseWriter, r *http.Request) {
|
||||
storageRoles := []models.Role{models.RoleAdministrator, models.RoleOperator}
|
||||
methodHandler(
|
||||
func(w http.ResponseWriter, r *http.Request) { a.handleGetDataset(w, r) },
|
||||
func(w http.ResponseWriter, r *http.Request) { a.handleCreateDataset(w, r) },
|
||||
func(w http.ResponseWriter, r *http.Request) { a.handleUpdateDataset(w, r) },
|
||||
func(w http.ResponseWriter, r *http.Request) { a.handleDeleteDataset(w, r) },
|
||||
func(w http.ResponseWriter, r *http.Request) {
|
||||
a.requireRole(storageRoles...)(http.HandlerFunc(a.handleCreateDataset)).ServeHTTP(w, r)
|
||||
},
|
||||
func(w http.ResponseWriter, r *http.Request) {
|
||||
a.requireRole(storageRoles...)(http.HandlerFunc(a.handleUpdateDataset)).ServeHTTP(w, r)
|
||||
},
|
||||
func(w http.ResponseWriter, r *http.Request) {
|
||||
a.requireRole(storageRoles...)(http.HandlerFunc(a.handleDeleteDataset)).ServeHTTP(w, r)
|
||||
},
|
||||
nil,
|
||||
)(w, r)
|
||||
}
|
||||
|
||||
// handleZVOLOps routes ZVOL operations by method
|
||||
func (a *App) handleZVOLOps(w http.ResponseWriter, r *http.Request) {
|
||||
storageRoles := []models.Role{models.RoleAdministrator, models.RoleOperator}
|
||||
methodHandler(
|
||||
func(w http.ResponseWriter, r *http.Request) { a.handleGetZVOL(w, r) },
|
||||
func(w http.ResponseWriter, r *http.Request) { a.handleCreateZVOL(w, r) },
|
||||
func(w http.ResponseWriter, r *http.Request) {
|
||||
a.requireRole(storageRoles...)(http.HandlerFunc(a.handleCreateZVOL)).ServeHTTP(w, r)
|
||||
},
|
||||
nil,
|
||||
func(w http.ResponseWriter, r *http.Request) { a.handleDeleteZVOL(w, r) },
|
||||
func(w http.ResponseWriter, r *http.Request) {
|
||||
a.requireRole(storageRoles...)(http.HandlerFunc(a.handleDeleteZVOL)).ServeHTTP(w, r)
|
||||
},
|
||||
nil,
|
||||
)(w, r)
|
||||
}
|
||||
|
||||
// handleSnapshotOps routes snapshot operations by method
|
||||
func (a *App) handleSnapshotOps(w http.ResponseWriter, r *http.Request) {
|
||||
storageRoles := []models.Role{models.RoleAdministrator, models.RoleOperator}
|
||||
// Check if it's a restore operation
|
||||
if strings.HasSuffix(r.URL.Path, "/restore") {
|
||||
if r.Method == http.MethodPost {
|
||||
a.handleRestoreSnapshot(w, r)
|
||||
a.requireRole(storageRoles...)(http.HandlerFunc(a.handleRestoreSnapshot)).ServeHTTP(w, r)
|
||||
} else {
|
||||
writeError(w, errors.ErrBadRequest("method not allowed"))
|
||||
}
|
||||
@@ -158,42 +180,67 @@ func (a *App) handleSnapshotOps(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
methodHandler(
|
||||
func(w http.ResponseWriter, r *http.Request) { a.handleGetSnapshot(w, r) },
|
||||
func(w http.ResponseWriter, r *http.Request) { a.handleCreateSnapshot(w, r) },
|
||||
func(w http.ResponseWriter, r *http.Request) {
|
||||
a.requireRole(storageRoles...)(http.HandlerFunc(a.handleCreateSnapshot)).ServeHTTP(w, r)
|
||||
},
|
||||
nil,
|
||||
func(w http.ResponseWriter, r *http.Request) { a.handleDeleteSnapshot(w, r) },
|
||||
func(w http.ResponseWriter, r *http.Request) {
|
||||
a.requireRole(storageRoles...)(http.HandlerFunc(a.handleDeleteSnapshot)).ServeHTTP(w, r)
|
||||
},
|
||||
nil,
|
||||
)(w, r)
|
||||
}
|
||||
|
||||
// handleSnapshotPolicyOps routes snapshot policy operations by method
|
||||
func (a *App) handleSnapshotPolicyOps(w http.ResponseWriter, r *http.Request) {
|
||||
storageRoles := []models.Role{models.RoleAdministrator, models.RoleOperator}
|
||||
methodHandler(
|
||||
func(w http.ResponseWriter, r *http.Request) { a.handleGetSnapshotPolicy(w, r) },
|
||||
func(w http.ResponseWriter, r *http.Request) { a.handleCreateSnapshotPolicy(w, r) },
|
||||
func(w http.ResponseWriter, r *http.Request) { a.handleUpdateSnapshotPolicy(w, r) },
|
||||
func(w http.ResponseWriter, r *http.Request) { a.handleDeleteSnapshotPolicy(w, r) },
|
||||
func(w http.ResponseWriter, r *http.Request) {
|
||||
a.requireRole(storageRoles...)(http.HandlerFunc(a.handleCreateSnapshotPolicy)).ServeHTTP(w, r)
|
||||
},
|
||||
func(w http.ResponseWriter, r *http.Request) {
|
||||
a.requireRole(storageRoles...)(http.HandlerFunc(a.handleUpdateSnapshotPolicy)).ServeHTTP(w, r)
|
||||
},
|
||||
func(w http.ResponseWriter, r *http.Request) {
|
||||
a.requireRole(storageRoles...)(http.HandlerFunc(a.handleDeleteSnapshotPolicy)).ServeHTTP(w, r)
|
||||
},
|
||||
nil,
|
||||
)(w, r)
|
||||
}
|
||||
|
||||
// handleSMBShareOps routes SMB share operations by method
|
||||
func (a *App) handleSMBShareOps(w http.ResponseWriter, r *http.Request) {
|
||||
storageRoles := []models.Role{models.RoleAdministrator, models.RoleOperator}
|
||||
methodHandler(
|
||||
func(w http.ResponseWriter, r *http.Request) { a.handleGetSMBShare(w, r) },
|
||||
func(w http.ResponseWriter, r *http.Request) { a.handleCreateSMBShare(w, r) },
|
||||
func(w http.ResponseWriter, r *http.Request) { a.handleUpdateSMBShare(w, r) },
|
||||
func(w http.ResponseWriter, r *http.Request) { a.handleDeleteSMBShare(w, r) },
|
||||
func(w http.ResponseWriter, r *http.Request) {
|
||||
a.requireRole(storageRoles...)(http.HandlerFunc(a.handleCreateSMBShare)).ServeHTTP(w, r)
|
||||
},
|
||||
func(w http.ResponseWriter, r *http.Request) {
|
||||
a.requireRole(storageRoles...)(http.HandlerFunc(a.handleUpdateSMBShare)).ServeHTTP(w, r)
|
||||
},
|
||||
func(w http.ResponseWriter, r *http.Request) {
|
||||
a.requireRole(storageRoles...)(http.HandlerFunc(a.handleDeleteSMBShare)).ServeHTTP(w, r)
|
||||
},
|
||||
nil,
|
||||
)(w, r)
|
||||
}
|
||||
|
||||
// handleNFSExportOps routes NFS export operations by method
|
||||
func (a *App) handleNFSExportOps(w http.ResponseWriter, r *http.Request) {
|
||||
storageRoles := []models.Role{models.RoleAdministrator, models.RoleOperator}
|
||||
methodHandler(
|
||||
func(w http.ResponseWriter, r *http.Request) { a.handleGetNFSExport(w, r) },
|
||||
func(w http.ResponseWriter, r *http.Request) { a.handleCreateNFSExport(w, r) },
|
||||
func(w http.ResponseWriter, r *http.Request) { a.handleUpdateNFSExport(w, r) },
|
||||
func(w http.ResponseWriter, r *http.Request) { a.handleDeleteNFSExport(w, r) },
|
||||
func(w http.ResponseWriter, r *http.Request) {
|
||||
a.requireRole(storageRoles...)(http.HandlerFunc(a.handleCreateNFSExport)).ServeHTTP(w, r)
|
||||
},
|
||||
func(w http.ResponseWriter, r *http.Request) {
|
||||
a.requireRole(storageRoles...)(http.HandlerFunc(a.handleUpdateNFSExport)).ServeHTTP(w, r)
|
||||
},
|
||||
func(w http.ResponseWriter, r *http.Request) {
|
||||
a.requireRole(storageRoles...)(http.HandlerFunc(a.handleDeleteNFSExport)).ServeHTTP(w, r)
|
||||
},
|
||||
nil,
|
||||
)(w, r)
|
||||
}
|
||||
@@ -206,6 +253,7 @@ func (a *App) handleBackupOps(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
storageRoles := []models.Role{models.RoleAdministrator, models.RoleOperator}
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
// Check if it's a verify request
|
||||
@@ -217,12 +265,12 @@ func (a *App) handleBackupOps(w http.ResponseWriter, r *http.Request) {
|
||||
case http.MethodPost:
|
||||
// Restore backup (POST /api/v1/backups/{id}/restore)
|
||||
if strings.HasSuffix(r.URL.Path, "/restore") {
|
||||
a.handleRestoreBackup(w, r)
|
||||
a.requireRole(storageRoles...)(http.HandlerFunc(a.handleRestoreBackup)).ServeHTTP(w, r)
|
||||
} else {
|
||||
writeError(w, errors.ErrBadRequest("invalid backup operation"))
|
||||
}
|
||||
case http.MethodDelete:
|
||||
a.handleDeleteBackup(w, r)
|
||||
a.requireRole(storageRoles...)(http.HandlerFunc(a.handleDeleteBackup)).ServeHTTP(w, r)
|
||||
default:
|
||||
writeError(w, errors.ErrBadRequest("method not allowed"))
|
||||
}
|
||||
@@ -244,9 +292,10 @@ func (a *App) handleISCSITargetOps(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
storageRoles := []models.Role{models.RoleAdministrator, models.RoleOperator}
|
||||
if strings.HasSuffix(r.URL.Path, "/luns") {
|
||||
if r.Method == http.MethodPost {
|
||||
a.handleAddLUN(w, r)
|
||||
a.requireRole(storageRoles...)(http.HandlerFunc(a.handleAddLUN)).ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
writeError(w, errors.NewAPIError(errors.ErrCodeBadRequest, "method not allowed", http.StatusMethodNotAllowed))
|
||||
@@ -255,7 +304,7 @@ func (a *App) handleISCSITargetOps(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
if strings.HasSuffix(r.URL.Path, "/luns/remove") {
|
||||
if r.Method == http.MethodPost {
|
||||
a.handleRemoveLUN(w, r)
|
||||
a.requireRole(storageRoles...)(http.HandlerFunc(a.handleRemoveLUN)).ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
writeError(w, errors.NewAPIError(errors.ErrCodeBadRequest, "method not allowed", http.StatusMethodNotAllowed))
|
||||
@@ -265,8 +314,12 @@ func (a *App) handleISCSITargetOps(w http.ResponseWriter, r *http.Request) {
|
||||
methodHandler(
|
||||
func(w http.ResponseWriter, r *http.Request) { a.handleGetISCSITarget(w, r) },
|
||||
nil,
|
||||
func(w http.ResponseWriter, r *http.Request) { a.handleUpdateISCSITarget(w, r) },
|
||||
func(w http.ResponseWriter, r *http.Request) { a.handleDeleteISCSITarget(w, r) },
|
||||
func(w http.ResponseWriter, r *http.Request) {
|
||||
a.requireRole(storageRoles...)(http.HandlerFunc(a.handleUpdateISCSITarget)).ServeHTTP(w, r)
|
||||
},
|
||||
func(w http.ResponseWriter, r *http.Request) {
|
||||
a.requireRole(storageRoles...)(http.HandlerFunc(a.handleDeleteISCSITarget)).ServeHTTP(w, r)
|
||||
},
|
||||
nil,
|
||||
)(w, r)
|
||||
}
|
||||
@@ -304,22 +357,68 @@ func (a *App) handleUserOps(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// handleVTLDriveOps routes VTL drive operations by method
|
||||
func (a *App) handleVTLDriveOps(w http.ResponseWriter, r *http.Request) {
|
||||
storageRoles := []models.Role{models.RoleAdministrator, models.RoleOperator}
|
||||
methodHandler(
|
||||
func(w http.ResponseWriter, r *http.Request) { a.handleGetVTLDrive(w, r) },
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
func(w http.ResponseWriter, r *http.Request) {
|
||||
a.requireRole(storageRoles...)(http.HandlerFunc(a.handleUpdateVTLDrive)).ServeHTTP(w, r)
|
||||
},
|
||||
func(w http.ResponseWriter, r *http.Request) {
|
||||
a.requireRole(storageRoles...)(http.HandlerFunc(a.handleDeleteVTLDrive)).ServeHTTP(w, r)
|
||||
},
|
||||
nil,
|
||||
)(w, r)
|
||||
}
|
||||
|
||||
// handleVTLTapeOps routes VTL tape operations by method
|
||||
func (a *App) handleVTLTapeOps(w http.ResponseWriter, r *http.Request) {
|
||||
storageRoles := []models.Role{models.RoleAdministrator, models.RoleOperator}
|
||||
methodHandler(
|
||||
func(w http.ResponseWriter, r *http.Request) { a.handleGetVTLTape(w, r) },
|
||||
func(w http.ResponseWriter, r *http.Request) { a.handleCreateVTLTape(w, r) },
|
||||
func(w http.ResponseWriter, r *http.Request) {
|
||||
a.requireRole(storageRoles...)(http.HandlerFunc(a.handleCreateVTLTape)).ServeHTTP(w, r)
|
||||
},
|
||||
nil,
|
||||
func(w http.ResponseWriter, r *http.Request) { a.handleDeleteVTLTape(w, r) },
|
||||
func(w http.ResponseWriter, r *http.Request) {
|
||||
a.requireRole(storageRoles...)(http.HandlerFunc(a.handleDeleteVTLTape)).ServeHTTP(w, r)
|
||||
},
|
||||
nil,
|
||||
)(w, r)
|
||||
}
|
||||
|
||||
// handleMediaChangerOps routes media changer operations by method
|
||||
func (a *App) handleMediaChangerOps(w http.ResponseWriter, r *http.Request) {
|
||||
storageRoles := []models.Role{models.RoleAdministrator, models.RoleOperator}
|
||||
methodHandler(
|
||||
func(w http.ResponseWriter, r *http.Request) {
|
||||
// Get single changer by ID
|
||||
libraryIDStr := pathParam(r, "/api/v1/vtl/changers/")
|
||||
libraryID, err := strconv.Atoi(libraryIDStr)
|
||||
if err != nil || libraryID <= 0 {
|
||||
writeError(w, errors.ErrValidation("invalid library_id"))
|
||||
return
|
||||
}
|
||||
changers, err := a.vtlService.ListMediaChangers()
|
||||
if err != nil {
|
||||
writeError(w, errors.ErrInternal(fmt.Sprintf("failed to list changers: %v", err)))
|
||||
return
|
||||
}
|
||||
for _, changer := range changers {
|
||||
if changer.LibraryID == libraryID {
|
||||
writeJSON(w, http.StatusOK, changer)
|
||||
return
|
||||
}
|
||||
}
|
||||
writeError(w, errors.ErrNotFound(fmt.Sprintf("media changer %d not found", libraryID)))
|
||||
},
|
||||
nil,
|
||||
func(w http.ResponseWriter, r *http.Request) {
|
||||
a.requireRole(storageRoles...)(http.HandlerFunc(a.handleUpdateMediaChanger)).ServeHTTP(w, r)
|
||||
},
|
||||
func(w http.ResponseWriter, r *http.Request) {
|
||||
a.requireRole(storageRoles...)(http.HandlerFunc(a.handleDeleteMediaChanger)).ServeHTTP(w, r)
|
||||
},
|
||||
nil,
|
||||
)(w, r)
|
||||
}
|
||||
|
||||
@@ -65,9 +65,14 @@ func (a *App) routes() {
|
||||
a.mux.HandleFunc("/api/openapi.yaml", a.handleOpenAPISpec)
|
||||
|
||||
// Backup & Restore
|
||||
// Define allowed roles for storage operations (Administrator and Operator, not Viewer)
|
||||
storageRoles := []models.Role{models.RoleAdministrator, models.RoleOperator}
|
||||
|
||||
a.mux.HandleFunc("/api/v1/backups", methodHandler(
|
||||
func(w http.ResponseWriter, r *http.Request) { a.handleListBackups(w, r) },
|
||||
func(w http.ResponseWriter, r *http.Request) { a.handleCreateBackup(w, r) },
|
||||
func(w http.ResponseWriter, r *http.Request) {
|
||||
a.requireRole(storageRoles...)(http.HandlerFunc(a.handleCreateBackup)).ServeHTTP(w, r)
|
||||
},
|
||||
nil, nil, nil,
|
||||
))
|
||||
a.mux.HandleFunc("/api/v1/backups/", a.handleBackupOps)
|
||||
@@ -85,7 +90,9 @@ func (a *App) routes() {
|
||||
))
|
||||
a.mux.HandleFunc("/api/v1/pools", methodHandler(
|
||||
func(w http.ResponseWriter, r *http.Request) { a.handleListPools(w, r) },
|
||||
func(w http.ResponseWriter, r *http.Request) { a.handleCreatePool(w, r) },
|
||||
func(w http.ResponseWriter, r *http.Request) {
|
||||
a.requireRole(storageRoles...)(http.HandlerFunc(a.handleCreatePool)).ServeHTTP(w, r)
|
||||
},
|
||||
nil, nil, nil,
|
||||
))
|
||||
a.mux.HandleFunc("/api/v1/pools/available", methodHandler(
|
||||
@@ -94,21 +101,27 @@ func (a *App) routes() {
|
||||
))
|
||||
a.mux.HandleFunc("/api/v1/pools/import", methodHandler(
|
||||
nil,
|
||||
func(w http.ResponseWriter, r *http.Request) { a.handleImportPool(w, r) },
|
||||
func(w http.ResponseWriter, r *http.Request) {
|
||||
a.requireRole(storageRoles...)(http.HandlerFunc(a.handleImportPool)).ServeHTTP(w, r)
|
||||
},
|
||||
nil, nil, nil,
|
||||
))
|
||||
a.mux.HandleFunc("/api/v1/pools/", a.handlePoolOps)
|
||||
|
||||
a.mux.HandleFunc("/api/v1/datasets", methodHandler(
|
||||
func(w http.ResponseWriter, r *http.Request) { a.handleListDatasets(w, r) },
|
||||
func(w http.ResponseWriter, r *http.Request) { a.handleCreateDataset(w, r) },
|
||||
func(w http.ResponseWriter, r *http.Request) {
|
||||
a.requireRole(storageRoles...)(http.HandlerFunc(a.handleCreateDataset)).ServeHTTP(w, r)
|
||||
},
|
||||
nil, nil, nil,
|
||||
))
|
||||
a.mux.HandleFunc("/api/v1/datasets/", a.handleDatasetOps)
|
||||
|
||||
a.mux.HandleFunc("/api/v1/zvols", methodHandler(
|
||||
func(w http.ResponseWriter, r *http.Request) { a.handleListZVOLs(w, r) },
|
||||
func(w http.ResponseWriter, r *http.Request) { a.handleCreateZVOL(w, r) },
|
||||
func(w http.ResponseWriter, r *http.Request) {
|
||||
a.requireRole(storageRoles...)(http.HandlerFunc(a.handleCreateZVOL)).ServeHTTP(w, r)
|
||||
},
|
||||
nil, nil, nil,
|
||||
))
|
||||
a.mux.HandleFunc("/api/v1/zvols/", a.handleZVOLOps)
|
||||
@@ -116,13 +129,17 @@ func (a *App) routes() {
|
||||
// Snapshot Management
|
||||
a.mux.HandleFunc("/api/v1/snapshots", methodHandler(
|
||||
func(w http.ResponseWriter, r *http.Request) { a.handleListSnapshots(w, r) },
|
||||
func(w http.ResponseWriter, r *http.Request) { a.handleCreateSnapshot(w, r) },
|
||||
func(w http.ResponseWriter, r *http.Request) {
|
||||
a.requireRole(storageRoles...)(http.HandlerFunc(a.handleCreateSnapshot)).ServeHTTP(w, r)
|
||||
},
|
||||
nil, nil, nil,
|
||||
))
|
||||
a.mux.HandleFunc("/api/v1/snapshots/", a.handleSnapshotOps)
|
||||
a.mux.HandleFunc("/api/v1/snapshot-policies", methodHandler(
|
||||
func(w http.ResponseWriter, r *http.Request) { a.handleListSnapshotPolicies(w, r) },
|
||||
func(w http.ResponseWriter, r *http.Request) { a.handleCreateSnapshotPolicy(w, r) },
|
||||
func(w http.ResponseWriter, r *http.Request) {
|
||||
a.requireRole(storageRoles...)(http.HandlerFunc(a.handleCreateSnapshotPolicy)).ServeHTTP(w, r)
|
||||
},
|
||||
nil, nil, nil,
|
||||
))
|
||||
a.mux.HandleFunc("/api/v1/snapshot-policies/", a.handleSnapshotPolicyOps)
|
||||
@@ -130,7 +147,9 @@ func (a *App) routes() {
|
||||
// Storage Services - SMB
|
||||
a.mux.HandleFunc("/api/v1/shares/smb", methodHandler(
|
||||
func(w http.ResponseWriter, r *http.Request) { a.handleListSMBShares(w, r) },
|
||||
func(w http.ResponseWriter, r *http.Request) { a.handleCreateSMBShare(w, r) },
|
||||
func(w http.ResponseWriter, r *http.Request) {
|
||||
a.requireRole(storageRoles...)(http.HandlerFunc(a.handleCreateSMBShare)).ServeHTTP(w, r)
|
||||
},
|
||||
nil, nil, nil,
|
||||
))
|
||||
a.mux.HandleFunc("/api/v1/shares/smb/", a.handleSMBShareOps)
|
||||
@@ -138,7 +157,9 @@ func (a *App) routes() {
|
||||
// Storage Services - NFS
|
||||
a.mux.HandleFunc("/api/v1/exports/nfs", methodHandler(
|
||||
func(w http.ResponseWriter, r *http.Request) { a.handleListNFSExports(w, r) },
|
||||
func(w http.ResponseWriter, r *http.Request) { a.handleCreateNFSExport(w, r) },
|
||||
func(w http.ResponseWriter, r *http.Request) {
|
||||
a.requireRole(storageRoles...)(http.HandlerFunc(a.handleCreateNFSExport)).ServeHTTP(w, r)
|
||||
},
|
||||
nil, nil, nil,
|
||||
))
|
||||
a.mux.HandleFunc("/api/v1/exports/nfs/", a.handleNFSExportOps)
|
||||
@@ -146,7 +167,9 @@ func (a *App) routes() {
|
||||
// Storage Services - iSCSI
|
||||
a.mux.HandleFunc("/api/v1/iscsi/targets", methodHandler(
|
||||
func(w http.ResponseWriter, r *http.Request) { a.handleListISCSITargets(w, r) },
|
||||
func(w http.ResponseWriter, r *http.Request) { a.handleCreateISCSITarget(w, r) },
|
||||
func(w http.ResponseWriter, r *http.Request) {
|
||||
a.requireRole(storageRoles...)(http.HandlerFunc(a.handleCreateISCSITarget)).ServeHTTP(w, r)
|
||||
},
|
||||
nil, nil, nil,
|
||||
))
|
||||
a.mux.HandleFunc("/api/v1/iscsi/targets/", a.handleISCSITargetOps)
|
||||
@@ -158,24 +181,36 @@ func (a *App) routes() {
|
||||
))
|
||||
a.mux.HandleFunc("/api/v1/vtl/drives", methodHandler(
|
||||
func(w http.ResponseWriter, r *http.Request) { a.handleListVTLDrives(w, r) },
|
||||
nil, nil, nil, nil,
|
||||
func(w http.ResponseWriter, r *http.Request) {
|
||||
storageRoles := []models.Role{models.RoleAdministrator, models.RoleOperator}
|
||||
a.requireRole(storageRoles...)(http.HandlerFunc(a.handleCreateVTLDrive)).ServeHTTP(w, r)
|
||||
},
|
||||
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) },
|
||||
func(w http.ResponseWriter, r *http.Request) {
|
||||
a.requireRole(storageRoles...)(http.HandlerFunc(a.handleCreateVTLTape)).ServeHTTP(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) },
|
||||
func(w http.ResponseWriter, r *http.Request) {
|
||||
a.requireRole(storageRoles...)(http.HandlerFunc(a.handleVTLServiceControl)).ServeHTTP(w, r)
|
||||
},
|
||||
nil, nil, nil,
|
||||
))
|
||||
a.mux.HandleFunc("/api/v1/vtl/changers", methodHandler(
|
||||
func(w http.ResponseWriter, r *http.Request) { a.handleListVTLMediaChangers(w, r) },
|
||||
nil, nil, nil, nil,
|
||||
func(w http.ResponseWriter, r *http.Request) {
|
||||
a.requireRole(storageRoles...)(http.HandlerFunc(a.handleCreateMediaChanger)).ServeHTTP(w, r)
|
||||
},
|
||||
nil, nil, nil,
|
||||
))
|
||||
a.mux.HandleFunc("/api/v1/vtl/changers/", a.handleMediaChangerOps)
|
||||
a.mux.HandleFunc("/api/v1/vtl/devices/iscsi", methodHandler(
|
||||
func(w http.ResponseWriter, r *http.Request) { a.handleListVTLDevicesForISCSI(w, r) },
|
||||
nil, nil, nil, nil,
|
||||
@@ -186,12 +221,16 @@ func (a *App) routes() {
|
||||
))
|
||||
a.mux.HandleFunc("/api/v1/vtl/tape/load", methodHandler(
|
||||
nil,
|
||||
func(w http.ResponseWriter, r *http.Request) { a.handleLoadTape(w, r) },
|
||||
func(w http.ResponseWriter, r *http.Request) {
|
||||
a.requireRole(storageRoles...)(http.HandlerFunc(a.handleLoadTape)).ServeHTTP(w, r)
|
||||
},
|
||||
nil, nil, nil,
|
||||
))
|
||||
a.mux.HandleFunc("/api/v1/vtl/tape/eject", methodHandler(
|
||||
nil,
|
||||
func(w http.ResponseWriter, r *http.Request) { a.handleEjectTape(w, r) },
|
||||
func(w http.ResponseWriter, r *http.Request) {
|
||||
a.requireRole(storageRoles...)(http.HandlerFunc(a.handleEjectTape)).ServeHTTP(w, r)
|
||||
},
|
||||
nil, nil, nil,
|
||||
))
|
||||
|
||||
|
||||
@@ -286,6 +286,193 @@ func (a *App) handleEjectTape(w http.ResponseWriter, r *http.Request) {
|
||||
})
|
||||
}
|
||||
|
||||
// handleCreateMediaChanger creates a new media changer/library
|
||||
func (a *App) handleCreateMediaChanger(w http.ResponseWriter, r *http.Request) {
|
||||
var req struct {
|
||||
LibraryID int `json:"library_id"`
|
||||
Vendor string `json:"vendor"`
|
||||
Product string `json:"product"`
|
||||
Serial string `json:"serial"`
|
||||
NumSlots int `json:"num_slots"`
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeError(w, errors.ErrValidation(fmt.Sprintf("invalid request body: %v", err)))
|
||||
return
|
||||
}
|
||||
|
||||
if req.LibraryID <= 0 {
|
||||
writeError(w, errors.ErrValidation("library_id must be greater than 0"))
|
||||
return
|
||||
}
|
||||
|
||||
if req.NumSlots <= 0 {
|
||||
req.NumSlots = 10 // Default number of slots
|
||||
}
|
||||
|
||||
if err := a.vtlService.AddMediaChanger(req.LibraryID, req.Vendor, req.Product, req.Serial, req.NumSlots); err != nil {
|
||||
log.Printf("create media changer error: %v", err)
|
||||
writeError(w, errors.ErrInternal(fmt.Sprintf("failed to create media changer: %v", err)))
|
||||
return
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusCreated, map[string]interface{}{
|
||||
"message": "Media changer created successfully",
|
||||
"library_id": req.LibraryID,
|
||||
})
|
||||
}
|
||||
|
||||
// handleUpdateMediaChanger updates a media changer/library configuration
|
||||
func (a *App) handleUpdateMediaChanger(w http.ResponseWriter, r *http.Request) {
|
||||
libraryIDStr := pathParam(r, "/api/v1/vtl/changers/")
|
||||
libraryID, err := strconv.Atoi(libraryIDStr)
|
||||
if err != nil || libraryID <= 0 {
|
||||
writeError(w, errors.ErrValidation("invalid library_id"))
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Vendor string `json:"vendor"`
|
||||
Product string `json:"product"`
|
||||
Serial string `json:"serial"`
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeError(w, errors.ErrValidation(fmt.Sprintf("invalid request body: %v", err)))
|
||||
return
|
||||
}
|
||||
|
||||
if err := a.vtlService.UpdateMediaChanger(libraryID, req.Vendor, req.Product, req.Serial); err != nil {
|
||||
log.Printf("update media changer error: %v", err)
|
||||
writeError(w, errors.ErrInternal(fmt.Sprintf("failed to update media changer: %v", err)))
|
||||
return
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, map[string]interface{}{
|
||||
"message": "Media changer updated successfully",
|
||||
"library_id": libraryID,
|
||||
})
|
||||
}
|
||||
|
||||
// handleDeleteMediaChanger removes a media changer/library
|
||||
func (a *App) handleDeleteMediaChanger(w http.ResponseWriter, r *http.Request) {
|
||||
libraryIDStr := pathParam(r, "/api/v1/vtl/changers/")
|
||||
libraryID, err := strconv.Atoi(libraryIDStr)
|
||||
if err != nil || libraryID <= 0 {
|
||||
writeError(w, errors.ErrValidation("invalid library_id"))
|
||||
return
|
||||
}
|
||||
|
||||
if err := a.vtlService.RemoveMediaChanger(libraryID); err != nil {
|
||||
log.Printf("delete media changer error: %v", err)
|
||||
writeError(w, errors.ErrInternal(fmt.Sprintf("failed to delete media changer: %v", err)))
|
||||
return
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, map[string]interface{}{
|
||||
"message": "Media changer deleted successfully",
|
||||
"library_id": libraryID,
|
||||
})
|
||||
}
|
||||
|
||||
// handleCreateVTLDrive creates a new drive
|
||||
func (a *App) handleCreateVTLDrive(w http.ResponseWriter, r *http.Request) {
|
||||
var req struct {
|
||||
DriveID int `json:"drive_id"`
|
||||
LibraryID int `json:"library_id"`
|
||||
SlotID int `json:"slot_id"`
|
||||
Vendor string `json:"vendor"`
|
||||
Product string `json:"product"`
|
||||
Serial string `json:"serial"`
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeError(w, errors.ErrValidation(fmt.Sprintf("invalid request body: %v", err)))
|
||||
return
|
||||
}
|
||||
|
||||
if req.DriveID <= 0 {
|
||||
writeError(w, errors.ErrValidation("drive_id must be greater than 0"))
|
||||
return
|
||||
}
|
||||
|
||||
if req.LibraryID <= 0 {
|
||||
writeError(w, errors.ErrValidation("library_id must be greater than 0"))
|
||||
return
|
||||
}
|
||||
|
||||
if req.SlotID <= 0 {
|
||||
writeError(w, errors.ErrValidation("slot_id must be greater than 0"))
|
||||
return
|
||||
}
|
||||
|
||||
if err := a.vtlService.AddDrive(req.DriveID, req.LibraryID, req.SlotID, req.Vendor, req.Product, req.Serial); err != nil {
|
||||
log.Printf("create VTL drive error: %v", err)
|
||||
writeError(w, errors.ErrInternal(fmt.Sprintf("failed to create VTL drive: %v", err)))
|
||||
return
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusCreated, map[string]interface{}{
|
||||
"message": "Drive created successfully",
|
||||
"drive_id": req.DriveID,
|
||||
})
|
||||
}
|
||||
|
||||
// handleUpdateVTLDrive updates a drive configuration
|
||||
func (a *App) handleUpdateVTLDrive(w http.ResponseWriter, r *http.Request) {
|
||||
driveIDStr := pathParam(r, "id")
|
||||
driveID, err := strconv.Atoi(driveIDStr)
|
||||
if err != nil || driveID <= 0 {
|
||||
writeError(w, errors.ErrValidation("invalid drive_id"))
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
LibraryID int `json:"library_id"`
|
||||
SlotID int `json:"slot_id"`
|
||||
Vendor string `json:"vendor"`
|
||||
Product string `json:"product"`
|
||||
Serial string `json:"serial"`
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeError(w, errors.ErrValidation(fmt.Sprintf("invalid request body: %v", err)))
|
||||
return
|
||||
}
|
||||
|
||||
if err := a.vtlService.UpdateDrive(driveID, req.LibraryID, req.SlotID, req.Vendor, req.Product, req.Serial); err != nil {
|
||||
log.Printf("update VTL drive error: %v", err)
|
||||
writeError(w, errors.ErrInternal(fmt.Sprintf("failed to update VTL drive: %v", err)))
|
||||
return
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, map[string]string{
|
||||
"message": "Drive updated successfully",
|
||||
"drive_id": fmt.Sprintf("%d", driveID),
|
||||
})
|
||||
}
|
||||
|
||||
// handleDeleteVTLDrive removes a drive
|
||||
func (a *App) handleDeleteVTLDrive(w http.ResponseWriter, r *http.Request) {
|
||||
driveIDStr := pathParam(r, "id")
|
||||
driveID, err := strconv.Atoi(driveIDStr)
|
||||
if err != nil || driveID <= 0 {
|
||||
writeError(w, errors.ErrValidation("invalid drive_id"))
|
||||
return
|
||||
}
|
||||
|
||||
if err := a.vtlService.RemoveDrive(driveID); err != nil {
|
||||
log.Printf("delete VTL drive error: %v", err)
|
||||
writeError(w, errors.ErrInternal(fmt.Sprintf("failed to delete VTL drive: %v", err)))
|
||||
return
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, map[string]string{
|
||||
"message": "Drive deleted successfully",
|
||||
"drive_id": fmt.Sprintf("%d", driveID),
|
||||
})
|
||||
}
|
||||
|
||||
// handleListVTLDevicesForISCSI returns all tape devices (drives and medium changers) for iSCSI passthrough
|
||||
func (a *App) handleListVTLDevicesForISCSI(w http.ResponseWriter, r *http.Request) {
|
||||
devices := []map[string]interface{}{}
|
||||
|
||||
@@ -740,7 +740,12 @@ func (s *VTLService) ListMediaChangers() ([]models.VTLMediaChanger, error) {
|
||||
|
||||
// Parse device.conf to get libraries
|
||||
deviceConfig, err := s.parseDeviceConfig()
|
||||
if err != nil {
|
||||
log.Printf("Warning: failed to parse device.conf: %v", err)
|
||||
}
|
||||
|
||||
if err == nil && len(deviceConfig.Libraries) > 0 {
|
||||
log.Printf("Found %d libraries in device.conf", len(deviceConfig.Libraries))
|
||||
// Count drives per library
|
||||
drivesPerLibrary := make(map[int]int)
|
||||
for _, drive := range deviceConfig.Drives {
|
||||
@@ -750,11 +755,37 @@ func (s *VTLService) ListMediaChangers() ([]models.VTLMediaChanger, error) {
|
||||
// Count slots per library from library_contents
|
||||
slotsPerLibrary := make(map[int]int)
|
||||
for _, lib := range deviceConfig.Libraries {
|
||||
slotMap, err := s.parseLibraryContents(lib.ID)
|
||||
if err == nil {
|
||||
slotsPerLibrary[lib.ID] = len(slotMap)
|
||||
// Count all slots (including empty ones) by reading file directly
|
||||
contentsPath := fmt.Sprintf("/etc/mhvtl/library_contents.%d", lib.ID)
|
||||
if file, err := os.Open(contentsPath); err == nil {
|
||||
scanner := bufio.NewScanner(file)
|
||||
slotRegex := regexp.MustCompile(`^Slot\s+(\d+):`)
|
||||
maxSlot := 0
|
||||
for scanner.Scan() {
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
if line == "" || strings.HasPrefix(line, "#") {
|
||||
continue
|
||||
}
|
||||
if matches := slotRegex.FindStringSubmatch(line); len(matches) >= 2 {
|
||||
if slotID, err := strconv.Atoi(matches[1]); err == nil && slotID > maxSlot {
|
||||
maxSlot = slotID
|
||||
}
|
||||
}
|
||||
}
|
||||
file.Close()
|
||||
if maxSlot > 0 {
|
||||
slotsPerLibrary[lib.ID] = maxSlot
|
||||
} else {
|
||||
// Fallback: try parseLibraryContents
|
||||
slotMap, _ := s.parseLibraryContents(lib.ID)
|
||||
slotsPerLibrary[lib.ID] = len(slotMap)
|
||||
if slotsPerLibrary[lib.ID] == 0 {
|
||||
slotsPerLibrary[lib.ID] = 10 // Default
|
||||
}
|
||||
}
|
||||
} else {
|
||||
slotsPerLibrary[lib.ID] = 10 // Default
|
||||
// File doesn't exist, use default
|
||||
slotsPerLibrary[lib.ID] = 10
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1026,6 +1057,542 @@ func (s *VTLService) parseDeviceConfig() (*DeviceConfig, error) {
|
||||
return config, scanner.Err()
|
||||
}
|
||||
|
||||
// writeDeviceConfig writes device.conf from DeviceConfig struct
|
||||
func (s *VTLService) writeDeviceConfig(config *DeviceConfig) error {
|
||||
// Ensure filesystem is writable (remount if needed)
|
||||
parentDir := filepath.Dir(s.deviceConfigPath)
|
||||
if err := s.ensureWritableFilesystem(parentDir); err != nil {
|
||||
log.Printf("Warning: failed to ensure writable filesystem for %s: %v", parentDir, err)
|
||||
// Continue anyway, might still work
|
||||
}
|
||||
|
||||
// Create backup of existing config
|
||||
backupPath := s.deviceConfigPath + ".backup"
|
||||
if _, err := os.Stat(s.deviceConfigPath); err == nil {
|
||||
// File exists, create backup
|
||||
if err := exec.Command("cp", s.deviceConfigPath, backupPath).Run(); err != nil {
|
||||
log.Printf("Warning: failed to create backup of device.conf: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Try to create file, if it fails due to read-only, try remount again
|
||||
file, err := os.Create(s.deviceConfigPath)
|
||||
if err != nil {
|
||||
// Check if it's a read-only filesystem error
|
||||
if strings.Contains(err.Error(), "read-only") || strings.Contains(err.Error(), "read only") {
|
||||
log.Printf("Filesystem is read-only, attempting remount for %s", parentDir)
|
||||
if remountErr := s.remountReadWrite(parentDir); remountErr != nil {
|
||||
return fmt.Errorf("failed to remount filesystem as read-write: %v", remountErr)
|
||||
}
|
||||
// Try again after remount
|
||||
file, err = os.Create(s.deviceConfigPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create device.conf after remount: %v", err)
|
||||
}
|
||||
} else {
|
||||
return fmt.Errorf("failed to create device.conf: %v", err)
|
||||
}
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
writer := bufio.NewWriter(file)
|
||||
|
||||
// Write libraries and their drives
|
||||
for _, lib := range config.Libraries {
|
||||
// Write library header
|
||||
writer.WriteString(fmt.Sprintf("Library: %d CHANNEL: 00 TARGET: 00 LUN: 00\n", lib.ID))
|
||||
if lib.Vendor != "" {
|
||||
writer.WriteString(fmt.Sprintf("Vendor identification: %s\n", lib.Vendor))
|
||||
} else {
|
||||
writer.WriteString("Vendor identification: STK\n")
|
||||
}
|
||||
if lib.Product != "" {
|
||||
writer.WriteString(fmt.Sprintf("Product identification: %s\n", lib.Product))
|
||||
} else {
|
||||
// Default product based on library ID
|
||||
if lib.ID == 30 {
|
||||
writer.WriteString("Product identification: L80\n")
|
||||
} else {
|
||||
writer.WriteString("Product identification: L700\n")
|
||||
}
|
||||
}
|
||||
if lib.Serial != "" {
|
||||
writer.WriteString(fmt.Sprintf("Unit serial number: %s\n", lib.Serial))
|
||||
} else {
|
||||
writer.WriteString(fmt.Sprintf("Unit serial number: %08d\n", lib.ID))
|
||||
}
|
||||
writer.WriteString("\n")
|
||||
|
||||
// Write drives for this library
|
||||
for _, drive := range config.Drives {
|
||||
if drive.LibraryID == lib.ID {
|
||||
// Calculate CHANNEL, TARGET, LUN from drive ID
|
||||
// Drive ID format: libraryID * 10 + slot (e.g., 11 = library 10, slot 1)
|
||||
channel := "00"
|
||||
target := fmt.Sprintf("%02d", (drive.ID%100)/10)
|
||||
lun := "00"
|
||||
|
||||
writer.WriteString(fmt.Sprintf("Drive: %d CHANNEL: %s TARGET: %s LUN: %s\n", drive.ID, channel, target, lun))
|
||||
writer.WriteString(fmt.Sprintf("Library ID: %d\n", drive.LibraryID))
|
||||
writer.WriteString(fmt.Sprintf("Slot: %d\n", drive.SlotID))
|
||||
if drive.Vendor != "" {
|
||||
writer.WriteString(fmt.Sprintf("Vendor identification: %s\n", drive.Vendor))
|
||||
} else {
|
||||
writer.WriteString("Vendor identification: IBM\n")
|
||||
}
|
||||
if drive.Product != "" {
|
||||
writer.WriteString(fmt.Sprintf("Product identification: %s\n", drive.Product))
|
||||
} else {
|
||||
writer.WriteString("Product identification: ULT3580-TD5\n")
|
||||
}
|
||||
if drive.Serial != "" {
|
||||
writer.WriteString(fmt.Sprintf("Unit serial number: %s\n", drive.Serial))
|
||||
} else {
|
||||
writer.WriteString(fmt.Sprintf("Unit serial number: %08d\n", drive.ID))
|
||||
}
|
||||
writer.WriteString("\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Flush and sync to ensure data is written to disk
|
||||
if err := writer.Flush(); err != nil {
|
||||
return fmt.Errorf("failed to flush device.conf: %v", err)
|
||||
}
|
||||
if err := file.Sync(); err != nil {
|
||||
log.Printf("Warning: failed to sync device.conf: %v", err)
|
||||
}
|
||||
|
||||
log.Printf("Successfully wrote device.conf with %d libraries and %d drives", len(config.Libraries), len(config.Drives))
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddMediaChanger adds a new media changer/library to device.conf
|
||||
func (s *VTLService) AddMediaChanger(libraryID int, vendor, product, serial string, numSlots int) error {
|
||||
if libraryID <= 0 {
|
||||
return fmt.Errorf("library ID must be greater than 0")
|
||||
}
|
||||
|
||||
// Parse existing config
|
||||
config, err := s.parseDeviceConfig()
|
||||
if err != nil {
|
||||
// If file doesn't exist, create new config
|
||||
config = &DeviceConfig{
|
||||
Libraries: []LibraryConfig{},
|
||||
Drives: []DriveConfig{},
|
||||
}
|
||||
}
|
||||
|
||||
// Check if library already exists
|
||||
for _, lib := range config.Libraries {
|
||||
if lib.ID == libraryID {
|
||||
return fmt.Errorf("library %d already exists", libraryID)
|
||||
}
|
||||
}
|
||||
|
||||
// Set defaults
|
||||
if vendor == "" {
|
||||
vendor = "STK"
|
||||
}
|
||||
if product == "" {
|
||||
if libraryID == 30 {
|
||||
product = "L80"
|
||||
} else {
|
||||
product = "L700"
|
||||
}
|
||||
}
|
||||
if serial == "" {
|
||||
serial = fmt.Sprintf("%08d", libraryID)
|
||||
}
|
||||
|
||||
// Add new library
|
||||
newLib := LibraryConfig{
|
||||
ID: libraryID,
|
||||
Vendor: vendor,
|
||||
Product: product,
|
||||
Serial: serial,
|
||||
}
|
||||
config.Libraries = append(config.Libraries, newLib)
|
||||
|
||||
// Write updated config
|
||||
if err := s.writeDeviceConfig(config); err != nil {
|
||||
return fmt.Errorf("failed to write device.conf: %v", err)
|
||||
}
|
||||
|
||||
// Create library_contents file if it doesn't exist
|
||||
contentsPath := fmt.Sprintf("/etc/mhvtl/library_contents.%d", libraryID)
|
||||
if _, err := os.Stat(contentsPath); os.IsNotExist(err) {
|
||||
// Ensure filesystem is writable
|
||||
parentDir := filepath.Dir(contentsPath)
|
||||
if err := s.ensureWritableFilesystem(parentDir); err != nil {
|
||||
log.Printf("Warning: failed to ensure writable filesystem for %s: %v", parentDir, err)
|
||||
}
|
||||
|
||||
file, err := os.Create(contentsPath)
|
||||
if err != nil {
|
||||
// If read-only, try remount
|
||||
if strings.Contains(err.Error(), "read-only") || strings.Contains(err.Error(), "read only") {
|
||||
if remountErr := s.remountReadWrite(parentDir); remountErr != nil {
|
||||
log.Printf("Warning: failed to remount for library_contents: %v", remountErr)
|
||||
} else {
|
||||
// Retry after remount
|
||||
file, err = os.Create(contentsPath)
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
log.Printf("Warning: failed to create library_contents.%d: %v", libraryID, err)
|
||||
} else {
|
||||
defer file.Close()
|
||||
// Write library_contents in correct format
|
||||
file.WriteString("VERSION: 2\n\n")
|
||||
// Drives will be added when drives are assigned to this library
|
||||
file.WriteString("Picker 1:\n\n")
|
||||
// Initialize with empty slots (MAP entries)
|
||||
for i := 1; i <= numSlots; i++ {
|
||||
file.WriteString(fmt.Sprintf("MAP %d:\n", i))
|
||||
}
|
||||
log.Printf("Created library_contents.%d with %d slots", libraryID, numSlots)
|
||||
}
|
||||
} else {
|
||||
defer file.Close()
|
||||
// Write library_contents in correct format
|
||||
file.WriteString("VERSION: 2\n\n")
|
||||
// Drives will be added when drives are assigned to this library
|
||||
file.WriteString("Picker 1:\n\n")
|
||||
// Initialize with empty slots (MAP entries)
|
||||
for i := 1; i <= numSlots; i++ {
|
||||
file.WriteString(fmt.Sprintf("MAP %d:\n", i))
|
||||
}
|
||||
log.Printf("Created library_contents.%d with %d slots", libraryID, numSlots)
|
||||
}
|
||||
}
|
||||
|
||||
// Restart mhvtl service to reflect changes (new library needs to be detected)
|
||||
if err := s.RestartService(); err != nil {
|
||||
log.Printf("Warning: failed to restart mhvtl service after adding media changer: %v", err)
|
||||
// Continue even if restart fails - library is added to config
|
||||
}
|
||||
|
||||
log.Printf("Added media changer: Library %d (%s %s)", libraryID, vendor, product)
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoveMediaChanger removes a media changer/library from device.conf
|
||||
func (s *VTLService) RemoveMediaChanger(libraryID int) error {
|
||||
if libraryID <= 0 {
|
||||
return fmt.Errorf("library ID must be greater than 0")
|
||||
}
|
||||
|
||||
// Parse existing config
|
||||
config, err := s.parseDeviceConfig()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse device.conf: %v", err)
|
||||
}
|
||||
|
||||
// Find and remove library
|
||||
found := false
|
||||
newLibraries := []LibraryConfig{}
|
||||
for _, lib := range config.Libraries {
|
||||
if lib.ID != libraryID {
|
||||
newLibraries = append(newLibraries, lib)
|
||||
} else {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
return fmt.Errorf("library %d not found", libraryID)
|
||||
}
|
||||
|
||||
// Remove all drives associated with this library
|
||||
newDrives := []DriveConfig{}
|
||||
for _, drive := range config.Drives {
|
||||
if drive.LibraryID != libraryID {
|
||||
newDrives = append(newDrives, drive)
|
||||
}
|
||||
}
|
||||
|
||||
config.Libraries = newLibraries
|
||||
config.Drives = newDrives
|
||||
|
||||
// Write updated config
|
||||
if err := s.writeDeviceConfig(config); err != nil {
|
||||
return fmt.Errorf("failed to write device.conf: %v", err)
|
||||
}
|
||||
|
||||
// Optionally remove library_contents file (but keep it for safety)
|
||||
// contentsPath := fmt.Sprintf("/etc/mhvtl/library_contents.%d", libraryID)
|
||||
// os.Remove(contentsPath)
|
||||
|
||||
// Restart mhvtl service to reflect changes (library removal)
|
||||
if err := s.RestartService(); err != nil {
|
||||
log.Printf("Warning: failed to restart mhvtl service after removing media changer: %v", err)
|
||||
// Continue even if restart fails - library is removed from config
|
||||
}
|
||||
|
||||
log.Printf("Removed media changer: Library %d", libraryID)
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateMediaChanger updates a media changer/library configuration
|
||||
func (s *VTLService) UpdateMediaChanger(libraryID int, vendor, product, serial string) error {
|
||||
if libraryID <= 0 {
|
||||
return fmt.Errorf("library ID must be greater than 0")
|
||||
}
|
||||
|
||||
// Parse existing config
|
||||
config, err := s.parseDeviceConfig()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse device.conf: %v", err)
|
||||
}
|
||||
|
||||
// Find and update library
|
||||
found := false
|
||||
for i := range config.Libraries {
|
||||
if config.Libraries[i].ID == libraryID {
|
||||
if vendor != "" {
|
||||
config.Libraries[i].Vendor = vendor
|
||||
}
|
||||
if product != "" {
|
||||
config.Libraries[i].Product = product
|
||||
}
|
||||
if serial != "" {
|
||||
config.Libraries[i].Serial = serial
|
||||
}
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
return fmt.Errorf("library %d not found", libraryID)
|
||||
}
|
||||
|
||||
// Write updated config
|
||||
if err := s.writeDeviceConfig(config); err != nil {
|
||||
return fmt.Errorf("failed to write device.conf: %v", err)
|
||||
}
|
||||
|
||||
// Restart mhvtl service to reflect changes (library config update)
|
||||
if err := s.RestartService(); err != nil {
|
||||
log.Printf("Warning: failed to restart mhvtl service after updating media changer: %v", err)
|
||||
// Continue even if restart fails - library is updated in config
|
||||
}
|
||||
|
||||
log.Printf("Updated media changer: Library %d", libraryID)
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddDrive adds a new drive to device.conf
|
||||
func (s *VTLService) AddDrive(driveID, libraryID, slotID int, vendor, product, serial string) error {
|
||||
if driveID <= 0 {
|
||||
return fmt.Errorf("drive ID must be greater than 0")
|
||||
}
|
||||
if libraryID <= 0 {
|
||||
return fmt.Errorf("library ID must be greater than 0")
|
||||
}
|
||||
if slotID <= 0 {
|
||||
return fmt.Errorf("slot ID must be greater than 0")
|
||||
}
|
||||
|
||||
// Parse existing config
|
||||
config, err := s.parseDeviceConfig()
|
||||
if err != nil {
|
||||
// If file doesn't exist, create new config
|
||||
config = &DeviceConfig{
|
||||
Libraries: []LibraryConfig{},
|
||||
Drives: []DriveConfig{},
|
||||
}
|
||||
}
|
||||
|
||||
// Check if library exists
|
||||
libraryExists := false
|
||||
for _, lib := range config.Libraries {
|
||||
if lib.ID == libraryID {
|
||||
libraryExists = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !libraryExists {
|
||||
return fmt.Errorf("library %d does not exist", libraryID)
|
||||
}
|
||||
|
||||
// Check if drive already exists
|
||||
for _, drive := range config.Drives {
|
||||
if drive.ID == driveID {
|
||||
return fmt.Errorf("drive %d already exists", driveID)
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate TARGET for this drive (used to determine device path)
|
||||
// TARGET = (driveID % 100) / 10
|
||||
// This ensures each drive gets a unique TARGET
|
||||
newTarget := (driveID % 100) / 10
|
||||
|
||||
// Check for TARGET conflict with existing drives
|
||||
// Each TARGET maps to a unique device path (/dev/stX), so we need to ensure uniqueness
|
||||
for _, drive := range config.Drives {
|
||||
existingTarget := (drive.ID % 100) / 10
|
||||
if existingTarget == newTarget {
|
||||
// TARGET maps to device: TARGET 01 -> /dev/st0, TARGET 02 -> /dev/st1, etc.
|
||||
deviceNum := newTarget - 1
|
||||
return fmt.Errorf("drive ID %d would conflict with drive %d (both use TARGET %02d, device /dev/st%d). Please use a different drive ID", driveID, drive.ID, existingTarget, deviceNum)
|
||||
}
|
||||
}
|
||||
|
||||
// Set defaults
|
||||
if vendor == "" {
|
||||
vendor = "IBM"
|
||||
}
|
||||
if product == "" {
|
||||
product = "ULT3580-TD5"
|
||||
}
|
||||
if serial == "" {
|
||||
serial = fmt.Sprintf("%08d", driveID)
|
||||
}
|
||||
|
||||
// Add new drive
|
||||
newDrive := DriveConfig{
|
||||
ID: driveID,
|
||||
LibraryID: libraryID,
|
||||
SlotID: slotID,
|
||||
Vendor: vendor,
|
||||
Product: product,
|
||||
Serial: serial,
|
||||
}
|
||||
config.Drives = append(config.Drives, newDrive)
|
||||
|
||||
// Write updated config
|
||||
if err := s.writeDeviceConfig(config); err != nil {
|
||||
return fmt.Errorf("failed to write device.conf: %v", err)
|
||||
}
|
||||
|
||||
// Update library_contents to add Drive entry if not exists
|
||||
if err := s.updateLibraryContentsForDrive(libraryID, driveID); err != nil {
|
||||
log.Printf("Warning: failed to update library_contents.%d for drive %d: %v", libraryID, driveID, err)
|
||||
// Continue even if library_contents update fails
|
||||
}
|
||||
|
||||
// Restart mhvtl service to reflect changes (new drive needs to be detected)
|
||||
if err := s.RestartService(); err != nil {
|
||||
log.Printf("Warning: failed to restart mhvtl service after adding drive: %v", err)
|
||||
// Continue even if restart fails - drive is added to config
|
||||
}
|
||||
|
||||
log.Printf("Added drive: Drive %d (Library %d, Slot %d)", driveID, libraryID, slotID)
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoveDrive removes a drive from device.conf
|
||||
func (s *VTLService) RemoveDrive(driveID int) error {
|
||||
if driveID <= 0 {
|
||||
return fmt.Errorf("drive ID must be greater than 0")
|
||||
}
|
||||
|
||||
// Parse existing config
|
||||
config, err := s.parseDeviceConfig()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse device.conf: %v", err)
|
||||
}
|
||||
|
||||
// Find and remove drive
|
||||
found := false
|
||||
newDrives := []DriveConfig{}
|
||||
for _, drive := range config.Drives {
|
||||
if drive.ID != driveID {
|
||||
newDrives = append(newDrives, drive)
|
||||
} else {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
return fmt.Errorf("drive %d not found", driveID)
|
||||
}
|
||||
|
||||
config.Drives = newDrives
|
||||
|
||||
// Write updated config
|
||||
if err := s.writeDeviceConfig(config); err != nil {
|
||||
return fmt.Errorf("failed to write device.conf: %v", err)
|
||||
}
|
||||
|
||||
// Restart mhvtl service to reflect changes (drive removal)
|
||||
if err := s.RestartService(); err != nil {
|
||||
log.Printf("Warning: failed to restart mhvtl service after removing drive: %v", err)
|
||||
// Continue even if restart fails - drive is removed from config
|
||||
}
|
||||
|
||||
log.Printf("Removed drive: Drive %d", driveID)
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateDrive updates a drive configuration
|
||||
func (s *VTLService) UpdateDrive(driveID, libraryID, slotID int, vendor, product, serial string) error {
|
||||
if driveID <= 0 {
|
||||
return fmt.Errorf("drive ID must be greater than 0")
|
||||
}
|
||||
|
||||
// Parse existing config
|
||||
config, err := s.parseDeviceConfig()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse device.conf: %v", err)
|
||||
}
|
||||
|
||||
// Find and update drive
|
||||
found := false
|
||||
for i := range config.Drives {
|
||||
if config.Drives[i].ID == driveID {
|
||||
if libraryID > 0 {
|
||||
// Check if library exists
|
||||
libraryExists := false
|
||||
for _, lib := range config.Libraries {
|
||||
if lib.ID == libraryID {
|
||||
libraryExists = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !libraryExists {
|
||||
return fmt.Errorf("library %d does not exist", libraryID)
|
||||
}
|
||||
config.Drives[i].LibraryID = libraryID
|
||||
}
|
||||
if slotID > 0 {
|
||||
config.Drives[i].SlotID = slotID
|
||||
}
|
||||
if vendor != "" {
|
||||
config.Drives[i].Vendor = vendor
|
||||
}
|
||||
if product != "" {
|
||||
config.Drives[i].Product = product
|
||||
}
|
||||
if serial != "" {
|
||||
config.Drives[i].Serial = serial
|
||||
}
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
return fmt.Errorf("drive %d not found", driveID)
|
||||
}
|
||||
|
||||
// Write updated config
|
||||
if err := s.writeDeviceConfig(config); err != nil {
|
||||
return fmt.Errorf("failed to write device.conf: %v", err)
|
||||
}
|
||||
|
||||
// Restart mhvtl service to reflect changes (drive config update)
|
||||
if err := s.RestartService(); err != nil {
|
||||
log.Printf("Warning: failed to restart mhvtl service after updating drive: %v", err)
|
||||
// Continue even if restart fails - drive is updated in config
|
||||
}
|
||||
|
||||
log.Printf("Updated drive: Drive %d", driveID)
|
||||
return nil
|
||||
}
|
||||
|
||||
// parseLibraryContents parses library_contents.X file
|
||||
func (s *VTLService) parseLibraryContents(libraryID int) (map[int]string, error) {
|
||||
// Map slot ID to barcode
|
||||
@@ -1067,32 +1634,31 @@ func (s *VTLService) parseLibraryContents(libraryID int) (map[int]string, error)
|
||||
}
|
||||
|
||||
// findDeviceForDrive finds device path for a drive ID
|
||||
// In mhvtl, device path is determined by the order drives appear in device.conf
|
||||
// TARGET number in device.conf maps to device: TARGET 01 -> /dev/st0, TARGET 02 -> /dev/st1, etc.
|
||||
func (s *VTLService) findDeviceForDrive(driveID int) string {
|
||||
// Try to find device in /sys/class/scsi_tape/
|
||||
tapePath := "/sys/class/scsi_tape"
|
||||
entries, err := os.ReadDir(tapePath)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("/dev/st%d", driveID%10) // Fallback
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
if !entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
deviceName := entry.Name()
|
||||
if !strings.HasPrefix(deviceName, "st") && !strings.HasPrefix(deviceName, "nst") {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if this device matches the drive ID
|
||||
// This is a simplified check - in real implementation, we'd need to map device to drive ID
|
||||
deviceID := s.getDriveIDFromDevice(deviceName)
|
||||
if deviceID == driveID {
|
||||
return fmt.Sprintf("/dev/%s", deviceName)
|
||||
// Parse device.conf to get TARGET for this drive
|
||||
deviceConfig, err := s.parseDeviceConfig()
|
||||
if err == nil {
|
||||
// Find the drive and calculate its TARGET
|
||||
for _, drive := range deviceConfig.Drives {
|
||||
if drive.ID == driveID {
|
||||
// Calculate TARGET from drive ID (same as in writeDeviceConfig)
|
||||
target := (driveID % 100) / 10
|
||||
// In mhvtl: TARGET 01 -> /dev/st0, TARGET 02 -> /dev/st1, TARGET 03 -> /dev/st2
|
||||
// Device numbering is 0-based, TARGET is effectively 1-based for the ones digit
|
||||
// So: TARGET 01 (ones=1) -> st0, TARGET 02 (ones=2) -> st1, etc.
|
||||
return fmt.Sprintf("/dev/st%d", target-1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Sprintf("/dev/st%d", driveID%10) // Fallback
|
||||
// Fallback: calculate TARGET from drive ID
|
||||
target := (driveID % 100) / 10
|
||||
if target > 0 {
|
||||
return fmt.Sprintf("/dev/st%d", target-1) // TARGET 01 -> st0, TARGET 02 -> st1
|
||||
}
|
||||
return fmt.Sprintf("/dev/st0") // Default fallback
|
||||
}
|
||||
|
||||
// checkMediaLoadedByDriveID checks if media is loaded in a drive by drive ID
|
||||
@@ -1477,6 +2043,200 @@ func (s *VTLService) addTapeToLibraryContents(libraryID int, slotID int, barcode
|
||||
return nil
|
||||
}
|
||||
|
||||
// updateLibraryContentsForDrive adds or updates Drive entry in library_contents file
|
||||
func (s *VTLService) updateLibraryContentsForDrive(libraryID, driveID int) error {
|
||||
contentsPath := fmt.Sprintf("/etc/mhvtl/library_contents.%d", libraryID)
|
||||
|
||||
// Ensure filesystem is writable
|
||||
parentDir := filepath.Dir(contentsPath)
|
||||
if err := s.ensureWritableFilesystem(parentDir); err != nil {
|
||||
log.Printf("Warning: failed to ensure writable filesystem for %s: %v", parentDir, err)
|
||||
}
|
||||
|
||||
// Check if file exists, if not create it with proper format
|
||||
file, err := os.OpenFile(contentsPath, os.O_RDWR|os.O_CREATE, 0644)
|
||||
if err != nil {
|
||||
// If read-only, try remount
|
||||
if strings.Contains(err.Error(), "read-only") || strings.Contains(err.Error(), "read only") {
|
||||
if remountErr := s.remountReadWrite(parentDir); remountErr != nil {
|
||||
return fmt.Errorf("failed to remount filesystem: %v", remountErr)
|
||||
}
|
||||
// Retry after remount
|
||||
file, err = os.OpenFile(contentsPath, os.O_RDWR|os.O_CREATE, 0644)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open library_contents file after remount: %v", err)
|
||||
}
|
||||
} else {
|
||||
return fmt.Errorf("failed to open library_contents file: %v", err)
|
||||
}
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// Read all lines
|
||||
var lines []string
|
||||
scanner := bufio.NewScanner(file)
|
||||
for scanner.Scan() {
|
||||
lines = append(lines, scanner.Text())
|
||||
}
|
||||
if err := scanner.Err(); err != nil {
|
||||
return fmt.Errorf("failed to read library_contents file: %v", err)
|
||||
}
|
||||
|
||||
// If file is empty or doesn't have VERSION, create new format
|
||||
if len(lines) == 0 {
|
||||
lines = []string{
|
||||
"VERSION: 2",
|
||||
"",
|
||||
"Picker 1:",
|
||||
"",
|
||||
}
|
||||
} else {
|
||||
// Check if VERSION exists
|
||||
hasVersion := false
|
||||
for _, line := range lines {
|
||||
if strings.Contains(strings.TrimSpace(line), "VERSION:") {
|
||||
hasVersion = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasVersion {
|
||||
// Prepend VERSION
|
||||
lines = append([]string{"VERSION: 2", ""}, lines...)
|
||||
}
|
||||
}
|
||||
|
||||
// Find drive number (sequential number for drives in this library)
|
||||
// Count existing drives
|
||||
driveRegex := regexp.MustCompile(`^Drive\s+(\d+):`)
|
||||
maxDriveNum := 0
|
||||
driveExists := false
|
||||
driveNum := 0
|
||||
|
||||
for _, line := range lines {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if matches := driveRegex.FindStringSubmatch(trimmed); len(matches) >= 2 {
|
||||
if num, err := strconv.Atoi(matches[1]); err == nil {
|
||||
if num > maxDriveNum {
|
||||
maxDriveNum = num
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if this drive already exists (by checking device.conf drive ID mapping)
|
||||
// For now, we'll use sequential numbering
|
||||
// Find if drive entry already exists by checking all drives in device.conf for this library
|
||||
deviceConfig, err := s.parseDeviceConfig()
|
||||
if err == nil {
|
||||
driveIndex := 1
|
||||
for _, drive := range deviceConfig.Drives {
|
||||
if drive.LibraryID == libraryID {
|
||||
if drive.ID == driveID {
|
||||
driveNum = driveIndex
|
||||
break
|
||||
}
|
||||
driveIndex++
|
||||
}
|
||||
}
|
||||
if driveNum == 0 {
|
||||
driveNum = maxDriveNum + 1
|
||||
}
|
||||
} else {
|
||||
driveNum = maxDriveNum + 1
|
||||
}
|
||||
|
||||
// Check if Drive entry already exists
|
||||
drivePattern := fmt.Sprintf("Drive %d:", driveNum)
|
||||
for _, line := range lines {
|
||||
if strings.Contains(strings.TrimSpace(line), drivePattern) {
|
||||
driveExists = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// If drive already exists, no need to update
|
||||
if driveExists {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Build new lines
|
||||
var newLines []string
|
||||
insertedDrive := false
|
||||
afterVersion := false
|
||||
pickerFound := false
|
||||
|
||||
for _, line := range lines {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
|
||||
// Track position after VERSION
|
||||
if strings.Contains(trimmed, "VERSION:") {
|
||||
afterVersion = true
|
||||
newLines = append(newLines, line)
|
||||
continue
|
||||
}
|
||||
|
||||
// Insert Drive entries after VERSION and before Picker
|
||||
if afterVersion && !insertedDrive {
|
||||
if strings.HasPrefix(trimmed, "Picker") {
|
||||
pickerFound = true
|
||||
// Insert drive before Picker
|
||||
newLines = append(newLines, fmt.Sprintf("Drive %d:", driveNum))
|
||||
newLines = append(newLines, "") // Empty line
|
||||
insertedDrive = true
|
||||
} else if strings.HasPrefix(trimmed, "MAP") && !pickerFound {
|
||||
// If no Picker found, insert before MAP
|
||||
newLines = append(newLines, fmt.Sprintf("Drive %d:", driveNum))
|
||||
newLines = append(newLines, "") // Empty line
|
||||
insertedDrive = true
|
||||
}
|
||||
}
|
||||
|
||||
newLines = append(newLines, line)
|
||||
}
|
||||
|
||||
// If drive wasn't inserted, add it at appropriate position
|
||||
if !insertedDrive {
|
||||
// Find position to insert (after VERSION, before Picker or MAP)
|
||||
insertPos := -1
|
||||
for idx, line := range newLines {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if strings.HasPrefix(trimmed, "Picker") || strings.HasPrefix(trimmed, "MAP") {
|
||||
insertPos = idx
|
||||
break
|
||||
}
|
||||
}
|
||||
if insertPos == -1 {
|
||||
// Add at end
|
||||
newLines = append(newLines, fmt.Sprintf("Drive %d:", driveNum))
|
||||
} else {
|
||||
// Insert at position
|
||||
newLines = append(newLines[:insertPos], append([]string{fmt.Sprintf("Drive %d:", driveNum), ""}, newLines[insertPos:]...)...)
|
||||
}
|
||||
}
|
||||
|
||||
// Write back to file
|
||||
if err := file.Truncate(0); err != nil {
|
||||
return fmt.Errorf("failed to truncate library_contents file: %v", err)
|
||||
}
|
||||
if _, err := file.Seek(0, 0); err != nil {
|
||||
return fmt.Errorf("failed to seek library_contents file: %v", err)
|
||||
}
|
||||
|
||||
writer := bufio.NewWriter(file)
|
||||
for _, line := range newLines {
|
||||
writer.WriteString(line + "\n")
|
||||
}
|
||||
if err := writer.Flush(); err != nil {
|
||||
return fmt.Errorf("failed to flush library_contents file: %v", err)
|
||||
}
|
||||
if err := file.Sync(); err != nil {
|
||||
log.Printf("Warning: failed to sync library_contents file: %v", err)
|
||||
}
|
||||
|
||||
log.Printf("Updated library_contents.%d with Drive %d entry", libraryID, driveNum)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ensureWritableFilesystem checks if filesystem is writable and remounts if needed
|
||||
func (s *VTLService) ensureWritableFilesystem(path string) error {
|
||||
// Try to create a test file to check if writable
|
||||
@@ -1498,9 +2258,12 @@ func (s *VTLService) remountReadWrite(path string) error {
|
||||
// Try /opt first, then /, then use findmnt
|
||||
var mountPoint string
|
||||
|
||||
// Check if path is under /opt
|
||||
// Check if path is under /opt or /etc
|
||||
if strings.HasPrefix(path, "/opt") {
|
||||
mountPoint = "/opt"
|
||||
} else if strings.HasPrefix(path, "/etc") {
|
||||
// For /etc, we need to remount root filesystem
|
||||
mountPoint = "/"
|
||||
} else {
|
||||
mountPoint = "/"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user