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