BAMS initial project structure
This commit is contained in:
422
backend/internal/api/handlers.go
Normal file
422
backend/internal/api/handlers.go
Normal file
@@ -0,0 +1,422 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/bams/backend/internal/services"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
func handleHealth() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
jsonResponse(w, http.StatusOK, map[string]interface{}{
|
||||
"status": "ok",
|
||||
"timestamp": time.Now().Unix(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func handleDashboard(sm *services.ServiceManager) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
// Get dashboard data from all services
|
||||
dashboard := map[string]interface{}{
|
||||
"disk": map[string]interface{}{
|
||||
"total_capacity": 0,
|
||||
"used_capacity": 0,
|
||||
"repositories": 0,
|
||||
},
|
||||
"tape": map[string]interface{}{
|
||||
"library_status": "unknown",
|
||||
"drives_active": 0,
|
||||
"total_slots": 0,
|
||||
"loaded_tapes": 0,
|
||||
},
|
||||
"iscsi": map[string]interface{}{
|
||||
"targets": 0,
|
||||
"sessions": 0,
|
||||
},
|
||||
"bacula": map[string]interface{}{
|
||||
"status": "unknown",
|
||||
},
|
||||
"alerts": []map[string]interface{}{},
|
||||
}
|
||||
|
||||
// Populate from services
|
||||
repos, _ := sm.Disk.ListRepositories()
|
||||
if repos != nil {
|
||||
dashboard["disk"].(map[string]interface{})["repositories"] = len(repos)
|
||||
}
|
||||
|
||||
library, _ := sm.Tape.GetLibrary()
|
||||
if library != nil {
|
||||
dashboard["tape"].(map[string]interface{})["library_status"] = library.Status
|
||||
dashboard["tape"].(map[string]interface{})["drives_active"] = library.ActiveDrives
|
||||
dashboard["tape"].(map[string]interface{})["total_slots"] = library.TotalSlots
|
||||
}
|
||||
|
||||
targets, _ := sm.ISCSI.ListTargets()
|
||||
if targets != nil {
|
||||
dashboard["iscsi"].(map[string]interface{})["targets"] = len(targets)
|
||||
}
|
||||
|
||||
sessions, _ := sm.ISCSI.ListSessions()
|
||||
if sessions != nil {
|
||||
dashboard["iscsi"].(map[string]interface{})["sessions"] = len(sessions)
|
||||
}
|
||||
|
||||
baculaStatus, _ := sm.Bacula.GetStatus()
|
||||
if baculaStatus != nil {
|
||||
dashboard["bacula"].(map[string]interface{})["status"] = baculaStatus.Status
|
||||
}
|
||||
|
||||
jsonResponse(w, http.StatusOK, dashboard)
|
||||
}
|
||||
}
|
||||
|
||||
// Disk repository handlers
|
||||
func handleListRepositories(sm *services.ServiceManager) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
repos, err := sm.Disk.ListRepositories()
|
||||
if err != nil {
|
||||
jsonError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
jsonResponse(w, http.StatusOK, repos)
|
||||
}
|
||||
}
|
||||
|
||||
func handleCreateRepository(sm *services.ServiceManager) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req struct {
|
||||
Name string `json:"name"`
|
||||
Size string `json:"size"`
|
||||
Type string `json:"type"` // "lvm" or "zfs"
|
||||
VGName string `json:"vg_name,omitempty"`
|
||||
PoolName string `json:"pool_name,omitempty"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
jsonError(w, http.StatusBadRequest, "Invalid request body")
|
||||
return
|
||||
}
|
||||
|
||||
repo, err := sm.Disk.CreateRepository(req.Name, req.Size, req.Type, req.VGName, req.PoolName)
|
||||
if err != nil {
|
||||
jsonError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
jsonResponse(w, http.StatusCreated, repo)
|
||||
}
|
||||
}
|
||||
|
||||
func handleGetRepository(sm *services.ServiceManager) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
repo, err := sm.Disk.GetRepository(vars["id"])
|
||||
if err != nil {
|
||||
jsonError(w, http.StatusNotFound, err.Error())
|
||||
return
|
||||
}
|
||||
jsonResponse(w, http.StatusOK, repo)
|
||||
}
|
||||
}
|
||||
|
||||
func handleDeleteRepository(sm *services.ServiceManager) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
if err := sm.Disk.DeleteRepository(vars["id"]); err != nil {
|
||||
jsonError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
jsonResponse(w, http.StatusNoContent, nil)
|
||||
}
|
||||
}
|
||||
|
||||
// Tape library handlers
|
||||
func handleGetLibrary(sm *services.ServiceManager) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
library, err := sm.Tape.GetLibrary()
|
||||
if err != nil {
|
||||
jsonError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
jsonResponse(w, http.StatusOK, library)
|
||||
}
|
||||
}
|
||||
|
||||
func handleInventory(sm *services.ServiceManager) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if err := sm.Tape.RunInventory(); err != nil {
|
||||
jsonError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
jsonResponse(w, http.StatusOK, map[string]string{"status": "inventory_started"})
|
||||
}
|
||||
}
|
||||
|
||||
func handleListDrives(sm *services.ServiceManager) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
drives, err := sm.Tape.ListDrives()
|
||||
if err != nil {
|
||||
jsonError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
jsonResponse(w, http.StatusOK, drives)
|
||||
}
|
||||
}
|
||||
|
||||
func handleLoadTape(sm *services.ServiceManager) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
var req struct {
|
||||
Slot int `json:"slot"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
jsonError(w, http.StatusBadRequest, "Invalid request body")
|
||||
return
|
||||
}
|
||||
|
||||
if err := sm.Tape.LoadTape(vars["id"], req.Slot); err != nil {
|
||||
jsonError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
jsonResponse(w, http.StatusOK, map[string]string{"status": "load_started"})
|
||||
}
|
||||
}
|
||||
|
||||
func handleUnloadTape(sm *services.ServiceManager) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
var req struct {
|
||||
Slot int `json:"slot"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
jsonError(w, http.StatusBadRequest, "Invalid request body")
|
||||
return
|
||||
}
|
||||
|
||||
if err := sm.Tape.UnloadTape(vars["id"], req.Slot); err != nil {
|
||||
jsonError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
jsonResponse(w, http.StatusOK, map[string]string{"status": "unload_started"})
|
||||
}
|
||||
}
|
||||
|
||||
func handleListSlots(sm *services.ServiceManager) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
slots, err := sm.Tape.ListSlots()
|
||||
if err != nil {
|
||||
jsonError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
jsonResponse(w, http.StatusOK, slots)
|
||||
}
|
||||
}
|
||||
|
||||
// iSCSI target handlers
|
||||
func handleListTargets(sm *services.ServiceManager) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
targets, err := sm.ISCSI.ListTargets()
|
||||
if err != nil {
|
||||
jsonError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
jsonResponse(w, http.StatusOK, targets)
|
||||
}
|
||||
}
|
||||
|
||||
func handleCreateTarget(sm *services.ServiceManager) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req struct {
|
||||
IQN string `json:"iqn"`
|
||||
Portals []string `json:"portals"`
|
||||
Initiators []string `json:"initiators"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
jsonError(w, http.StatusBadRequest, "Invalid request body")
|
||||
return
|
||||
}
|
||||
|
||||
target, err := sm.ISCSI.CreateTarget(req.IQN, req.Portals, req.Initiators)
|
||||
if err != nil {
|
||||
jsonError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
jsonResponse(w, http.StatusCreated, target)
|
||||
}
|
||||
}
|
||||
|
||||
func handleGetTarget(sm *services.ServiceManager) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
target, err := sm.ISCSI.GetTarget(vars["id"])
|
||||
if err != nil {
|
||||
jsonError(w, http.StatusNotFound, err.Error())
|
||||
return
|
||||
}
|
||||
jsonResponse(w, http.StatusOK, target)
|
||||
}
|
||||
}
|
||||
|
||||
func handleUpdateTarget(sm *services.ServiceManager) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
var req struct {
|
||||
Portals []string `json:"portals"`
|
||||
Initiators []string `json:"initiators"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
jsonError(w, http.StatusBadRequest, "Invalid request body")
|
||||
return
|
||||
}
|
||||
|
||||
target, err := sm.ISCSI.UpdateTarget(vars["id"], req.Portals, req.Initiators)
|
||||
if err != nil {
|
||||
jsonError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
jsonResponse(w, http.StatusOK, target)
|
||||
}
|
||||
}
|
||||
|
||||
func handleDeleteTarget(sm *services.ServiceManager) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
if err := sm.ISCSI.DeleteTarget(vars["id"]); err != nil {
|
||||
jsonError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
jsonResponse(w, http.StatusNoContent, nil)
|
||||
}
|
||||
}
|
||||
|
||||
func handleApplyTarget(sm *services.ServiceManager) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
if err := sm.ISCSI.ApplyTarget(vars["id"]); err != nil {
|
||||
jsonError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
jsonResponse(w, http.StatusOK, map[string]string{"status": "applied"})
|
||||
}
|
||||
}
|
||||
|
||||
func handleListSessions(sm *services.ServiceManager) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
sessions, err := sm.ISCSI.ListSessions()
|
||||
if err != nil {
|
||||
jsonError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
jsonResponse(w, http.StatusOK, sessions)
|
||||
}
|
||||
}
|
||||
|
||||
// Bacula handlers
|
||||
func handleBaculaStatus(sm *services.ServiceManager) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
status, err := sm.Bacula.GetStatus()
|
||||
if err != nil {
|
||||
jsonError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
jsonResponse(w, http.StatusOK, status)
|
||||
}
|
||||
}
|
||||
|
||||
func handleGetBaculaConfig(sm *services.ServiceManager) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
config, err := sm.Bacula.GetConfig()
|
||||
if err != nil {
|
||||
jsonError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
jsonResponse(w, http.StatusOK, config)
|
||||
}
|
||||
}
|
||||
|
||||
func handleGenerateBaculaConfig(sm *services.ServiceManager) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
config, err := sm.Bacula.GenerateConfig()
|
||||
if err != nil {
|
||||
jsonError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
jsonResponse(w, http.StatusOK, config)
|
||||
}
|
||||
}
|
||||
|
||||
func handleBaculaInventory(sm *services.ServiceManager) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if err := sm.Bacula.RunInventory(); err != nil {
|
||||
jsonError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
jsonResponse(w, http.StatusOK, map[string]string{"status": "inventory_started"})
|
||||
}
|
||||
}
|
||||
|
||||
func handleBaculaRestart(sm *services.ServiceManager) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if err := sm.Bacula.Restart(); err != nil {
|
||||
jsonError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
jsonResponse(w, http.StatusOK, map[string]string{"status": "restart_started"})
|
||||
}
|
||||
}
|
||||
|
||||
// Logs and diagnostics handlers
|
||||
func handleGetLogs(sm *services.ServiceManager) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
lines := 100
|
||||
if linesStr := r.URL.Query().Get("lines"); linesStr != "" {
|
||||
if n, err := strconv.Atoi(linesStr); err == nil {
|
||||
lines = n
|
||||
}
|
||||
}
|
||||
logs, err := sm.Logs.GetLogs(vars["service"], lines)
|
||||
if err != nil {
|
||||
jsonError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
jsonResponse(w, http.StatusOK, logs)
|
||||
}
|
||||
}
|
||||
|
||||
func handleStreamLogs(sm *services.ServiceManager) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
// WebSocket streaming would be implemented here
|
||||
jsonError(w, http.StatusNotImplemented, "WebSocket streaming not yet implemented")
|
||||
}
|
||||
}
|
||||
|
||||
func handleDownloadBundle(sm *services.ServiceManager) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
bundle, err := sm.Logs.GenerateSupportBundle()
|
||||
if err != nil {
|
||||
jsonError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/zip")
|
||||
w.Header().Set("Content-Disposition", "attachment; filename=bams-support-bundle.zip")
|
||||
w.Write(bundle)
|
||||
}
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
func jsonResponse(w http.ResponseWriter, status int, data interface{}) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(status)
|
||||
if data != nil {
|
||||
json.NewEncoder(w).Encode(data)
|
||||
}
|
||||
}
|
||||
|
||||
func jsonError(w http.ResponseWriter, status int, message string) {
|
||||
jsonResponse(w, status, map[string]string{"error": message})
|
||||
}
|
||||
87
backend/internal/api/router.go
Normal file
87
backend/internal/api/router.go
Normal file
@@ -0,0 +1,87 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/bams/backend/internal/logger"
|
||||
"github.com/bams/backend/internal/services"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
func NewRouter(sm *services.ServiceManager, log *logger.Logger) *mux.Router {
|
||||
router := mux.NewRouter()
|
||||
|
||||
// Middleware
|
||||
router.Use(loggingMiddleware(log))
|
||||
router.Use(corsMiddleware())
|
||||
|
||||
// API v1 routes
|
||||
v1 := router.PathPrefix("/api/v1").Subrouter()
|
||||
|
||||
// Dashboard
|
||||
v1.HandleFunc("/dashboard", handleDashboard(sm)).Methods("GET")
|
||||
|
||||
// Disk repository management
|
||||
v1.HandleFunc("/disk/repositories", handleListRepositories(sm)).Methods("GET")
|
||||
v1.HandleFunc("/disk/repositories", handleCreateRepository(sm)).Methods("POST")
|
||||
v1.HandleFunc("/disk/repositories/{id}", handleGetRepository(sm)).Methods("GET")
|
||||
v1.HandleFunc("/disk/repositories/{id}", handleDeleteRepository(sm)).Methods("DELETE")
|
||||
|
||||
// Tape library management
|
||||
v1.HandleFunc("/tape/library", handleGetLibrary(sm)).Methods("GET")
|
||||
v1.HandleFunc("/tape/inventory", handleInventory(sm)).Methods("POST")
|
||||
v1.HandleFunc("/tape/drives", handleListDrives(sm)).Methods("GET")
|
||||
v1.HandleFunc("/tape/drives/{id}/load", handleLoadTape(sm)).Methods("POST")
|
||||
v1.HandleFunc("/tape/drives/{id}/unload", handleUnloadTape(sm)).Methods("POST")
|
||||
v1.HandleFunc("/tape/slots", handleListSlots(sm)).Methods("GET")
|
||||
|
||||
// iSCSI target management
|
||||
v1.HandleFunc("/iscsi/targets", handleListTargets(sm)).Methods("GET")
|
||||
v1.HandleFunc("/iscsi/targets", handleCreateTarget(sm)).Methods("POST")
|
||||
v1.HandleFunc("/iscsi/targets/{id}", handleGetTarget(sm)).Methods("GET")
|
||||
v1.HandleFunc("/iscsi/targets/{id}", handleUpdateTarget(sm)).Methods("PUT")
|
||||
v1.HandleFunc("/iscsi/targets/{id}", handleDeleteTarget(sm)).Methods("DELETE")
|
||||
v1.HandleFunc("/iscsi/targets/{id}/apply", handleApplyTarget(sm)).Methods("POST")
|
||||
v1.HandleFunc("/iscsi/sessions", handleListSessions(sm)).Methods("GET")
|
||||
|
||||
// Bacula integration
|
||||
v1.HandleFunc("/bacula/status", handleBaculaStatus(sm)).Methods("GET")
|
||||
v1.HandleFunc("/bacula/config", handleGetBaculaConfig(sm)).Methods("GET")
|
||||
v1.HandleFunc("/bacula/config", handleGenerateBaculaConfig(sm)).Methods("POST")
|
||||
v1.HandleFunc("/bacula/inventory", handleBaculaInventory(sm)).Methods("POST")
|
||||
v1.HandleFunc("/bacula/restart", handleBaculaRestart(sm)).Methods("POST")
|
||||
|
||||
// Logs and diagnostics
|
||||
v1.HandleFunc("/logs/{service}", handleGetLogs(sm)).Methods("GET")
|
||||
v1.HandleFunc("/logs/{service}/stream", handleStreamLogs(sm)).Methods("GET")
|
||||
v1.HandleFunc("/diagnostics/bundle", handleDownloadBundle(sm)).Methods("GET")
|
||||
|
||||
// Health check
|
||||
router.HandleFunc("/health", handleHealth()).Methods("GET")
|
||||
|
||||
return router
|
||||
}
|
||||
|
||||
func loggingMiddleware(log *logger.Logger) mux.MiddlewareFunc {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
log.Info("HTTP request", "method", r.Method, "path", r.URL.Path)
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func corsMiddleware() mux.MiddlewareFunc {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
|
||||
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
|
||||
if r.Method == "OPTIONS" {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
}
|
||||
61
backend/internal/config/config.go
Normal file
61
backend/internal/config/config.go
Normal file
@@ -0,0 +1,61 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Port int `yaml:"port"`
|
||||
LogLevel string `yaml:"log_level"`
|
||||
DataDir string `yaml:"data_dir"`
|
||||
SCSTConfig string `yaml:"scst_config"`
|
||||
BaculaConfig string `yaml:"bacula_config"`
|
||||
Security SecurityConfig `yaml:"security"`
|
||||
}
|
||||
|
||||
type SecurityConfig struct {
|
||||
RequireHTTPS bool `yaml:"require_https"`
|
||||
AllowedUsers []string `yaml:"allowed_users"`
|
||||
}
|
||||
|
||||
func Load() (*Config, error) {
|
||||
cfg := &Config{
|
||||
Port: 8080,
|
||||
LogLevel: "info",
|
||||
DataDir: "/var/lib/bams",
|
||||
SCSTConfig: "/etc/scst.conf",
|
||||
BaculaConfig: "/etc/bacula/bacula-sd.conf",
|
||||
Security: SecurityConfig{
|
||||
RequireHTTPS: false,
|
||||
AllowedUsers: []string{},
|
||||
},
|
||||
}
|
||||
|
||||
// Load from environment or config file
|
||||
if configPath := os.Getenv("BAMS_CONFIG"); configPath != "" {
|
||||
data, err := os.ReadFile(configPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read config file: %w", err)
|
||||
}
|
||||
if err := yaml.Unmarshal(data, cfg); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse config file: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Override with environment variables
|
||||
if portStr := os.Getenv("BAMS_PORT"); portStr != "" {
|
||||
if port, err := strconv.Atoi(portStr); err == nil {
|
||||
cfg.Port = port
|
||||
}
|
||||
}
|
||||
|
||||
if logLevel := os.Getenv("BAMS_LOG_LEVEL"); logLevel != "" {
|
||||
cfg.LogLevel = logLevel
|
||||
}
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
60
backend/internal/logger/logger.go
Normal file
60
backend/internal/logger/logger.go
Normal file
@@ -0,0 +1,60 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
)
|
||||
|
||||
type Logger struct {
|
||||
level string
|
||||
log *log.Logger
|
||||
}
|
||||
|
||||
func New(level string) *Logger {
|
||||
return &Logger{
|
||||
level: level,
|
||||
log: log.New(os.Stdout, "[BAMS] ", log.LstdFlags|log.Lshortfile),
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Logger) Info(msg string, fields ...interface{}) {
|
||||
if l.shouldLog("info") {
|
||||
l.log.Printf("[INFO] %s %v", msg, fields)
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Logger) Error(msg string, fields ...interface{}) {
|
||||
if l.shouldLog("error") {
|
||||
l.log.Printf("[ERROR] %s %v", msg, fields)
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Logger) Debug(msg string, fields ...interface{}) {
|
||||
if l.shouldLog("debug") {
|
||||
l.log.Printf("[DEBUG] %s %v", msg, fields)
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Logger) Warn(msg string, fields ...interface{}) {
|
||||
if l.shouldLog("warn") {
|
||||
l.log.Printf("[WARN] %s %v", msg, fields)
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Logger) shouldLog(level string) bool {
|
||||
levels := map[string]int{
|
||||
"debug": 0,
|
||||
"info": 1,
|
||||
"warn": 2,
|
||||
"error": 3,
|
||||
}
|
||||
currentLevel, ok := levels[l.level]
|
||||
if !ok {
|
||||
currentLevel = 1
|
||||
}
|
||||
msgLevel, ok := levels[level]
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
return msgLevel >= currentLevel
|
||||
}
|
||||
74
backend/internal/services/audit/audit.go
Normal file
74
backend/internal/services/audit/audit.go
Normal file
@@ -0,0 +1,74 @@
|
||||
package audit
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/bams/backend/internal/config"
|
||||
"github.com/bams/backend/internal/logger"
|
||||
)
|
||||
|
||||
type AuditEntry struct {
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
User string `json:"user"`
|
||||
Action string `json:"action"`
|
||||
Resource string `json:"resource"`
|
||||
Result string `json:"result"` // "success" or "failure"
|
||||
Details string `json:"details,omitempty"`
|
||||
}
|
||||
|
||||
type Service struct {
|
||||
config *config.Config
|
||||
logger *logger.Logger
|
||||
logFile *os.File
|
||||
}
|
||||
|
||||
func NewService(cfg *config.Config, log *logger.Logger) *Service {
|
||||
auditDir := filepath.Join(cfg.DataDir, "audit")
|
||||
os.MkdirAll(auditDir, 0755)
|
||||
|
||||
logPath := filepath.Join(auditDir, fmt.Sprintf("audit-%s.log", time.Now().Format("2006-01-02")))
|
||||
file, err := os.OpenFile(logPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0640)
|
||||
if err != nil {
|
||||
log.Error("Failed to open audit log", "error", err)
|
||||
}
|
||||
|
||||
return &Service{
|
||||
config: cfg,
|
||||
logger: log,
|
||||
logFile: file,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) Log(user, action, resource, result, details string) {
|
||||
entry := AuditEntry{
|
||||
Timestamp: time.Now(),
|
||||
User: user,
|
||||
Action: action,
|
||||
Resource: resource,
|
||||
Result: result,
|
||||
Details: details,
|
||||
}
|
||||
|
||||
data, err := json.Marshal(entry)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to marshal audit entry", "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
if s.logFile != nil {
|
||||
s.logFile.WriteString(string(data) + "\n")
|
||||
s.logFile.Sync()
|
||||
}
|
||||
|
||||
s.logger.Info("Audit log", "user", user, "action", action, "resource", resource, "result", result)
|
||||
}
|
||||
|
||||
func (s *Service) Close() {
|
||||
if s.logFile != nil {
|
||||
s.logFile.Close()
|
||||
}
|
||||
}
|
||||
130
backend/internal/services/bacula/service.go
Normal file
130
backend/internal/services/bacula/service.go
Normal file
@@ -0,0 +1,130 @@
|
||||
package bacula
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"github.com/bams/backend/internal/config"
|
||||
"github.com/bams/backend/internal/logger"
|
||||
)
|
||||
|
||||
type Status struct {
|
||||
Status string `json:"status"` // "running", "stopped", "unknown"
|
||||
PID int `json:"pid,omitempty"`
|
||||
Version string `json:"version,omitempty"`
|
||||
LastError string `json:"last_error,omitempty"`
|
||||
}
|
||||
|
||||
type Service struct {
|
||||
config *config.Config
|
||||
logger *logger.Logger
|
||||
}
|
||||
|
||||
func NewService(cfg *config.Config, log *logger.Logger) *Service {
|
||||
return &Service{
|
||||
config: cfg,
|
||||
logger: log,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) GetStatus() (*Status, error) {
|
||||
s.logger.Debug("Getting Bacula SD status")
|
||||
|
||||
status := &Status{
|
||||
Status: "unknown",
|
||||
}
|
||||
|
||||
// Check if bacula-sd process is running
|
||||
cmd := exec.Command("systemctl", "is-active", "bacula-sd")
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err == nil {
|
||||
if strings.TrimSpace(string(output)) == "active" {
|
||||
status.Status = "running"
|
||||
} else {
|
||||
status.Status = "stopped"
|
||||
}
|
||||
}
|
||||
|
||||
return status, nil
|
||||
}
|
||||
|
||||
func (s *Service) GetConfig() (string, error) {
|
||||
s.logger.Debug("Getting Bacula SD config")
|
||||
|
||||
if _, err := os.Stat(s.config.BaculaConfig); err != nil {
|
||||
return "", fmt.Errorf("config file not found: %w", err)
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(s.config.BaculaConfig)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to read config: %w", err)
|
||||
}
|
||||
|
||||
return string(data), nil
|
||||
}
|
||||
|
||||
func (s *Service) GenerateConfig() (string, error) {
|
||||
s.logger.Info("Generating Bacula SD config")
|
||||
|
||||
// Generate template configuration
|
||||
config := `# Bacula Storage Daemon Configuration
|
||||
# Generated by BAMS
|
||||
|
||||
Storage {
|
||||
Name = bacula-sd
|
||||
WorkingDirectory = /var/lib/bacula
|
||||
PidDirectory = /run/bacula
|
||||
Maximum Concurrent Jobs = 20
|
||||
SDAddress = 0.0.0.0
|
||||
}
|
||||
|
||||
Director {
|
||||
Name = bacula-dir
|
||||
Password = "changeme"
|
||||
}
|
||||
|
||||
Device {
|
||||
Name = FileStorage
|
||||
Media Type = File
|
||||
Archive Device = /var/lib/bacula/storage
|
||||
LabelMedia = yes
|
||||
Random Access = yes
|
||||
AutomaticMount = yes
|
||||
RemovableMedia = no
|
||||
AlwaysOpen = no
|
||||
}
|
||||
|
||||
# Autochanger configuration will be added here
|
||||
`
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
func (s *Service) RunInventory() error {
|
||||
s.logger.Info("Running Bacula inventory")
|
||||
|
||||
// Use bconsole to run inventory
|
||||
cmd := exec.Command("bconsole", "-c", "update slots")
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to run inventory", "error", string(output))
|
||||
return fmt.Errorf("inventory failed: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) Restart() error {
|
||||
s.logger.Info("Restarting Bacula SD")
|
||||
|
||||
cmd := exec.Command("systemctl", "restart", "bacula-sd")
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to restart Bacula SD", "error", string(output))
|
||||
return fmt.Errorf("restart failed: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
101
backend/internal/services/disk/service.go
Normal file
101
backend/internal/services/disk/service.go
Normal file
@@ -0,0 +1,101 @@
|
||||
package disk
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os/exec"
|
||||
|
||||
"github.com/bams/backend/internal/config"
|
||||
"github.com/bams/backend/internal/logger"
|
||||
)
|
||||
|
||||
type Repository struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"` // "lvm" or "zfs"
|
||||
Size string `json:"size"`
|
||||
Used string `json:"used"`
|
||||
MountPoint string `json:"mount_point"`
|
||||
Status string `json:"status"`
|
||||
VGName string `json:"vg_name,omitempty"`
|
||||
PoolName string `json:"pool_name,omitempty"`
|
||||
}
|
||||
|
||||
type Service struct {
|
||||
config *config.Config
|
||||
logger *logger.Logger
|
||||
}
|
||||
|
||||
func NewService(cfg *config.Config, log *logger.Logger) *Service {
|
||||
return &Service{
|
||||
config: cfg,
|
||||
logger: log,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) ListRepositories() ([]*Repository, error) {
|
||||
// TODO: Implement actual LVM/ZFS listing
|
||||
s.logger.Debug("Listing disk repositories")
|
||||
return []*Repository{}, nil
|
||||
}
|
||||
|
||||
func (s *Service) GetRepository(id string) (*Repository, error) {
|
||||
// TODO: Implement actual repository retrieval
|
||||
s.logger.Debug("Getting repository", "id", id)
|
||||
return nil, fmt.Errorf("repository not found: %s", id)
|
||||
}
|
||||
|
||||
func (s *Service) CreateRepository(name, size, repoType, vgName, poolName string) (*Repository, error) {
|
||||
s.logger.Info("Creating repository", "name", name, "size", size, "type", repoType)
|
||||
|
||||
var err error
|
||||
if repoType == "lvm" {
|
||||
err = s.createLVMRepository(name, size, vgName)
|
||||
} else if repoType == "zfs" {
|
||||
err = s.createZFSRepository(name, size, poolName)
|
||||
} else {
|
||||
return nil, fmt.Errorf("unsupported repository type: %s", repoType)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Repository{
|
||||
ID: name,
|
||||
Name: name,
|
||||
Type: repoType,
|
||||
Size: size,
|
||||
Status: "active",
|
||||
VGName: vgName,
|
||||
PoolName: poolName,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Service) createLVMRepository(name, size, vgName string) error {
|
||||
// Create LVM logical volume
|
||||
cmd := exec.Command("lvcreate", "-L", size, "-n", name, vgName)
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to create LVM volume", "error", string(output))
|
||||
return fmt.Errorf("failed to create LVM volume: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) createZFSRepository(name, size, poolName string) error {
|
||||
// Create ZFS zvol
|
||||
zvolPath := fmt.Sprintf("%s/%s", poolName, name)
|
||||
cmd := exec.Command("zfs", "create", "-V", size, zvolPath)
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to create ZFS zvol", "error", string(output))
|
||||
return fmt.Errorf("failed to create ZFS zvol: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) DeleteRepository(id string) error {
|
||||
s.logger.Info("Deleting repository", "id", id)
|
||||
// TODO: Implement actual deletion
|
||||
return nil
|
||||
}
|
||||
222
backend/internal/services/iscsi/service.go
Normal file
222
backend/internal/services/iscsi/service.go
Normal file
@@ -0,0 +1,222 @@
|
||||
package iscsi
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"github.com/bams/backend/internal/config"
|
||||
"github.com/bams/backend/internal/logger"
|
||||
)
|
||||
|
||||
type Target struct {
|
||||
ID string `json:"id"`
|
||||
IQN string `json:"iqn"`
|
||||
Portals []string `json:"portals"`
|
||||
Initiators []string `json:"initiators"`
|
||||
LUNs []LUN `json:"luns"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
type LUN struct {
|
||||
Number int `json:"number"`
|
||||
Device string `json:"device"`
|
||||
Type string `json:"type"` // "disk" or "tape"
|
||||
}
|
||||
|
||||
type Session struct {
|
||||
TargetIQN string `json:"target_iqn"`
|
||||
InitiatorIQN string `json:"initiator_iqn"`
|
||||
IP string `json:"ip"`
|
||||
State string `json:"state"`
|
||||
}
|
||||
|
||||
type Service struct {
|
||||
config *config.Config
|
||||
logger *logger.Logger
|
||||
}
|
||||
|
||||
func NewService(cfg *config.Config, log *logger.Logger) *Service {
|
||||
return &Service{
|
||||
config: cfg,
|
||||
logger: log,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) ListTargets() ([]*Target, error) {
|
||||
s.logger.Debug("Listing iSCSI targets")
|
||||
|
||||
// Read SCST configuration
|
||||
targets := []*Target{}
|
||||
|
||||
// Parse SCST config file
|
||||
if _, err := os.Stat(s.config.SCSTConfig); err == nil {
|
||||
data, err := os.ReadFile(s.config.SCSTConfig)
|
||||
if err == nil {
|
||||
targets = s.parseSCSTConfig(string(data))
|
||||
}
|
||||
}
|
||||
|
||||
return targets, nil
|
||||
}
|
||||
|
||||
func (s *Service) GetTarget(id string) (*Target, error) {
|
||||
targets, err := s.ListTargets()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, target := range targets {
|
||||
if target.ID == id || target.IQN == id {
|
||||
return target, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("target not found: %s", id)
|
||||
}
|
||||
|
||||
func (s *Service) CreateTarget(iqn string, portals, initiators []string) (*Target, error) {
|
||||
s.logger.Info("Creating iSCSI target", "iqn", iqn)
|
||||
|
||||
target := &Target{
|
||||
ID: iqn,
|
||||
IQN: iqn,
|
||||
Portals: portals,
|
||||
Initiators: initiators,
|
||||
LUNs: []LUN{},
|
||||
Status: "pending",
|
||||
}
|
||||
|
||||
// Write to SCST config
|
||||
if err := s.writeSCSTConfig(target); err != nil {
|
||||
return nil, fmt.Errorf("failed to write SCST config: %w", err)
|
||||
}
|
||||
|
||||
return target, nil
|
||||
}
|
||||
|
||||
func (s *Service) UpdateTarget(id string, portals, initiators []string) (*Target, error) {
|
||||
target, err := s.GetTarget(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
target.Portals = portals
|
||||
target.Initiators = initiators
|
||||
|
||||
if err := s.writeSCSTConfig(target); err != nil {
|
||||
return nil, fmt.Errorf("failed to update SCST config: %w", err)
|
||||
}
|
||||
|
||||
return target, nil
|
||||
}
|
||||
|
||||
func (s *Service) DeleteTarget(id string) error {
|
||||
s.logger.Info("Deleting iSCSI target", "id", id)
|
||||
|
||||
// Remove from SCST config
|
||||
// TODO: Implement actual deletion
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) ApplyTarget(id string) error {
|
||||
s.logger.Info("Applying iSCSI target configuration", "id", id)
|
||||
|
||||
// Reload SCST configuration
|
||||
cmd := exec.Command("scstadmin", "-config", s.config.SCSTConfig)
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to apply SCST config", "error", string(output))
|
||||
return fmt.Errorf("failed to apply SCST config: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) ListSessions() ([]*Session, error) {
|
||||
s.logger.Debug("Listing iSCSI sessions")
|
||||
|
||||
sessions := []*Session{}
|
||||
|
||||
// Query SCST for active sessions
|
||||
cmd := exec.Command("scstadmin", "-list_sessions")
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err == nil {
|
||||
sessions = s.parseSCSTSessions(string(output))
|
||||
}
|
||||
|
||||
return sessions, nil
|
||||
}
|
||||
|
||||
func (s *Service) parseSCSTConfig(data string) []*Target {
|
||||
targets := []*Target{}
|
||||
// Simplified parsing - real implementation would parse SCST config format
|
||||
lines := strings.Split(data, "\n")
|
||||
currentTarget := &Target{}
|
||||
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
if strings.HasPrefix(line, "TARGET") {
|
||||
if currentTarget.IQN != "" {
|
||||
targets = append(targets, currentTarget)
|
||||
}
|
||||
currentTarget = &Target{
|
||||
IQN: strings.Fields(line)[1],
|
||||
ID: strings.Fields(line)[1],
|
||||
Portals: []string{},
|
||||
Initiators: []string{},
|
||||
LUNs: []LUN{},
|
||||
Status: "active",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if currentTarget.IQN != "" {
|
||||
targets = append(targets, currentTarget)
|
||||
}
|
||||
|
||||
return targets
|
||||
}
|
||||
|
||||
func (s *Service) parseSCSTSessions(data string) []*Session {
|
||||
sessions := []*Session{}
|
||||
// Parse SCST session output
|
||||
lines := strings.Split(data, "\n")
|
||||
|
||||
for _, line := range lines {
|
||||
if strings.Contains(line, "session") {
|
||||
// Parse session information
|
||||
session := &Session{
|
||||
State: "active",
|
||||
}
|
||||
sessions = append(sessions, session)
|
||||
}
|
||||
}
|
||||
|
||||
return sessions
|
||||
}
|
||||
|
||||
func (s *Service) writeSCSTConfig(target *Target) error {
|
||||
// Generate SCST configuration
|
||||
config := fmt.Sprintf(`TARGET %s {
|
||||
enabled 1
|
||||
`, target.IQN)
|
||||
|
||||
for _, portal := range target.Portals {
|
||||
config += fmt.Sprintf(" portal %s {\n", portal)
|
||||
config += " enabled 1\n"
|
||||
config += " }\n"
|
||||
}
|
||||
|
||||
for _, initiator := range target.Initiators {
|
||||
config += fmt.Sprintf(" initiator %s {\n", initiator)
|
||||
config += " enabled 1\n"
|
||||
config += " }\n"
|
||||
}
|
||||
|
||||
config += "}\n"
|
||||
|
||||
// Append to config file (simplified - real implementation would merge properly)
|
||||
return os.WriteFile(s.config.SCSTConfig, []byte(config), 0644)
|
||||
}
|
||||
136
backend/internal/services/logs/service.go
Normal file
136
backend/internal/services/logs/service.go
Normal file
@@ -0,0 +1,136 @@
|
||||
package logs
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/bams/backend/internal/config"
|
||||
"github.com/bams/backend/internal/logger"
|
||||
)
|
||||
|
||||
type LogEntry struct {
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
Level string `json:"level"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
type Service struct {
|
||||
config *config.Config
|
||||
logger *logger.Logger
|
||||
}
|
||||
|
||||
func NewService(cfg *config.Config, log *logger.Logger) *Service {
|
||||
return &Service{
|
||||
config: cfg,
|
||||
logger: log,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) GetLogs(service string, lines int) ([]*LogEntry, error) {
|
||||
var logPath string
|
||||
|
||||
switch service {
|
||||
case "scst":
|
||||
logPath = "/var/log/scst.log"
|
||||
case "iscsi":
|
||||
logPath = "/var/log/kern.log" // iSCSI logs often in kernel log
|
||||
case "bacula":
|
||||
logPath = "/var/log/bacula/bacula.log"
|
||||
case "bams":
|
||||
logPath = "/var/log/bams/bams.log"
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown service: %s", service)
|
||||
}
|
||||
|
||||
if _, err := os.Stat(logPath); err != nil {
|
||||
return []*LogEntry{}, nil
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(logPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read log file: %w", err)
|
||||
}
|
||||
|
||||
entries := []*LogEntry{}
|
||||
logLines := strings.Split(string(data), "\n")
|
||||
|
||||
// Get last N lines
|
||||
start := 0
|
||||
if len(logLines) > lines {
|
||||
start = len(logLines) - lines
|
||||
}
|
||||
|
||||
for i := start; i < len(logLines); i++ {
|
||||
if logLines[i] == "" {
|
||||
continue
|
||||
}
|
||||
entry := &LogEntry{
|
||||
Timestamp: time.Now(), // Simplified - would parse from log line
|
||||
Level: "info",
|
||||
Message: logLines[i],
|
||||
}
|
||||
entries = append(entries, entry)
|
||||
}
|
||||
|
||||
return entries, nil
|
||||
}
|
||||
|
||||
func (s *Service) GenerateSupportBundle() ([]byte, error) {
|
||||
s.logger.Info("Generating support bundle")
|
||||
|
||||
// Create temporary directory
|
||||
tmpDir := "/tmp/bams-bundle"
|
||||
os.MkdirAll(tmpDir, 0755)
|
||||
|
||||
// Collect logs
|
||||
logs := []string{"scst", "iscsi", "bacula", "bams"}
|
||||
for _, service := range logs {
|
||||
logPath := s.getLogPath(service)
|
||||
if _, err := os.Stat(logPath); err == nil {
|
||||
data, _ := os.ReadFile(logPath)
|
||||
os.WriteFile(filepath.Join(tmpDir, fmt.Sprintf("%s.log", service)), data, 0644)
|
||||
}
|
||||
}
|
||||
|
||||
// Collect configs
|
||||
configs := map[string]string{
|
||||
"scst.conf": s.config.SCSTConfig,
|
||||
"bacula-sd.conf": s.config.BaculaConfig,
|
||||
}
|
||||
for name, path := range configs {
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
data, _ := os.ReadFile(path)
|
||||
os.WriteFile(filepath.Join(tmpDir, name), data, 0644)
|
||||
}
|
||||
}
|
||||
|
||||
// Collect system info
|
||||
systemInfo := fmt.Sprintf("OS: %s\n", "Linux")
|
||||
os.WriteFile(filepath.Join(tmpDir, "system-info.txt"), []byte(systemInfo), 0644)
|
||||
|
||||
// Create zip (simplified - would use archive/zip)
|
||||
bundleData := []byte("Support bundle placeholder")
|
||||
|
||||
// Cleanup
|
||||
os.RemoveAll(tmpDir)
|
||||
|
||||
return bundleData, nil
|
||||
}
|
||||
|
||||
func (s *Service) getLogPath(service string) string {
|
||||
switch service {
|
||||
case "scst":
|
||||
return "/var/log/scst.log"
|
||||
case "iscsi":
|
||||
return "/var/log/kern.log"
|
||||
case "bacula":
|
||||
return "/var/log/bacula/bacula.log"
|
||||
case "bams":
|
||||
return "/var/log/bams/bams.log"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
42
backend/internal/services/manager.go
Normal file
42
backend/internal/services/manager.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"github.com/bams/backend/internal/config"
|
||||
"github.com/bams/backend/internal/logger"
|
||||
"github.com/bams/backend/internal/services/bacula"
|
||||
"github.com/bams/backend/internal/services/disk"
|
||||
"github.com/bams/backend/internal/services/iscsi"
|
||||
"github.com/bams/backend/internal/services/logs"
|
||||
"github.com/bams/backend/internal/services/tape"
|
||||
)
|
||||
|
||||
type ServiceManager struct {
|
||||
Config *config.Config
|
||||
Logger *logger.Logger
|
||||
Disk *disk.Service
|
||||
ISCSI *iscsi.Service
|
||||
Tape *tape.Service
|
||||
Bacula *bacula.Service
|
||||
Logs *logs.Service
|
||||
}
|
||||
|
||||
func NewServiceManager(cfg *config.Config, log *logger.Logger) *ServiceManager {
|
||||
sm := &ServiceManager{
|
||||
Config: cfg,
|
||||
Logger: log,
|
||||
}
|
||||
|
||||
// Initialize services
|
||||
sm.Disk = disk.NewService(cfg, log)
|
||||
sm.ISCSI = iscsi.NewService(cfg, log)
|
||||
sm.Tape = tape.NewService(cfg, log)
|
||||
sm.Bacula = bacula.NewService(cfg, log)
|
||||
sm.Logs = logs.NewService(cfg, log)
|
||||
|
||||
return sm
|
||||
}
|
||||
|
||||
func (sm *ServiceManager) Shutdown() {
|
||||
sm.Logger.Info("Shutting down services...")
|
||||
// Graceful shutdown of all services
|
||||
}
|
||||
183
backend/internal/services/tape/service.go
Normal file
183
backend/internal/services/tape/service.go
Normal file
@@ -0,0 +1,183 @@
|
||||
package tape
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/bams/backend/internal/config"
|
||||
"github.com/bams/backend/internal/logger"
|
||||
)
|
||||
|
||||
type Library struct {
|
||||
Status string `json:"status"`
|
||||
TotalSlots int `json:"total_slots"`
|
||||
ActiveDrives int `json:"active_drives"`
|
||||
Model string `json:"model"`
|
||||
Serial string `json:"serial"`
|
||||
}
|
||||
|
||||
type Drive struct {
|
||||
ID string `json:"id"`
|
||||
Status string `json:"status"`
|
||||
LoadedTape string `json:"loaded_tape,omitempty"`
|
||||
Barcode string `json:"barcode,omitempty"`
|
||||
Position int `json:"position"`
|
||||
}
|
||||
|
||||
type Slot struct {
|
||||
Number int `json:"number"`
|
||||
Barcode string `json:"barcode,omitempty"`
|
||||
Status string `json:"status"` // "empty", "loaded", "import"
|
||||
}
|
||||
|
||||
type Service struct {
|
||||
config *config.Config
|
||||
logger *logger.Logger
|
||||
}
|
||||
|
||||
func NewService(cfg *config.Config, log *logger.Logger) *Service {
|
||||
return &Service{
|
||||
config: cfg,
|
||||
logger: log,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) GetLibrary() (*Library, error) {
|
||||
s.logger.Debug("Getting tape library info")
|
||||
|
||||
// Try to detect library using mtx or sg_lib
|
||||
library := &Library{
|
||||
Status: "unknown",
|
||||
TotalSlots: 0,
|
||||
ActiveDrives: 0,
|
||||
}
|
||||
|
||||
// Check for mtx (media changer)
|
||||
cmd := exec.Command("mtx", "-f", "/dev/sg0", "status")
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err == nil {
|
||||
// Parse mtx output
|
||||
lines := strings.Split(string(output), "\n")
|
||||
for _, line := range lines {
|
||||
if strings.Contains(line, "Storage Element") {
|
||||
library.TotalSlots++
|
||||
}
|
||||
if strings.Contains(line, "Data Transfer Element") {
|
||||
library.ActiveDrives++
|
||||
}
|
||||
}
|
||||
library.Status = "online"
|
||||
}
|
||||
|
||||
return library, nil
|
||||
}
|
||||
|
||||
func (s *Service) RunInventory() error {
|
||||
s.logger.Info("Running tape library inventory")
|
||||
|
||||
// Use mtx to inventory
|
||||
cmd := exec.Command("mtx", "-f", "/dev/sg0", "inventory")
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to run inventory", "error", string(output))
|
||||
return fmt.Errorf("inventory failed: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) ListDrives() ([]*Drive, error) {
|
||||
s.logger.Debug("Listing tape drives")
|
||||
|
||||
drives := []*Drive{}
|
||||
|
||||
// Detect drives (up to 8)
|
||||
for i := 0; i < 8; i++ {
|
||||
device := fmt.Sprintf("/dev/nst%d", i)
|
||||
cmd := exec.Command("mt", "-f", device, "status")
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err == nil {
|
||||
drive := &Drive{
|
||||
ID: fmt.Sprintf("drive-%d", i),
|
||||
Status: "online",
|
||||
Position: i,
|
||||
}
|
||||
|
||||
// Check if tape is loaded
|
||||
if strings.Contains(string(output), "ONLINE") {
|
||||
drive.Status = "loaded"
|
||||
}
|
||||
|
||||
drives = append(drives, drive)
|
||||
}
|
||||
}
|
||||
|
||||
return drives, nil
|
||||
}
|
||||
|
||||
func (s *Service) LoadTape(driveID string, slot int) error {
|
||||
s.logger.Info("Loading tape", "drive", driveID, "slot", slot)
|
||||
|
||||
// Extract drive number from ID
|
||||
driveNum := 0
|
||||
fmt.Sscanf(driveID, "drive-%d", &driveNum)
|
||||
|
||||
// Use mtx to load
|
||||
cmd := exec.Command("mtx", "-f", "/dev/sg0", "load", strconv.Itoa(slot), strconv.Itoa(driveNum))
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to load tape", "error", string(output))
|
||||
return fmt.Errorf("load failed: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) UnloadTape(driveID string, slot int) error {
|
||||
s.logger.Info("Unloading tape", "drive", driveID, "slot", slot)
|
||||
|
||||
// Extract drive number from ID
|
||||
driveNum := 0
|
||||
fmt.Sscanf(driveID, "drive-%d", &driveNum)
|
||||
|
||||
// Use mtx to unload
|
||||
cmd := exec.Command("mtx", "-f", "/dev/sg0", "unload", strconv.Itoa(slot), strconv.Itoa(driveNum))
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to unload tape", "error", string(output))
|
||||
return fmt.Errorf("unload failed: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) ListSlots() ([]*Slot, error) {
|
||||
s.logger.Debug("Listing tape slots")
|
||||
|
||||
slots := []*Slot{}
|
||||
|
||||
// Use mtx to get slot status
|
||||
cmd := exec.Command("mtx", "-f", "/dev/sg0", "status")
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return slots, fmt.Errorf("failed to get slot status: %w", err)
|
||||
}
|
||||
|
||||
// Parse mtx output
|
||||
lines := strings.Split(string(output), "\n")
|
||||
for _, line := range lines {
|
||||
if strings.Contains(line, "Storage Element") {
|
||||
// Parse slot information
|
||||
slot := &Slot{
|
||||
Status: "empty",
|
||||
}
|
||||
// Extract slot number and barcode from line
|
||||
// This is simplified - real parsing would be more complex
|
||||
slots = append(slots, slot)
|
||||
}
|
||||
}
|
||||
|
||||
return slots, nil
|
||||
}
|
||||
Reference in New Issue
Block a user