BAMS initial project structure
This commit is contained in:
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