BAMS initial project structure

This commit is contained in:
2025-12-23 18:34:39 +00:00
parent e1df870f98
commit 861e0f65c3
24 changed files with 2495 additions and 0 deletions

View 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})
}

View 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)
})
}
}

View 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
}

View 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
}

View 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()
}
}

View 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
}

View 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
}

View 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)
}

View 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 ""
}
}

View 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
}

View 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
}