Files
calypso/backend/internal/tape_vtl/service.go
2025-12-27 14:13:27 +00:00

545 lines
16 KiB
Go

package tape_vtl
import (
"context"
"database/sql"
"fmt"
"os"
"path/filepath"
"time"
"github.com/atlasos/calypso/internal/common/database"
"github.com/atlasos/calypso/internal/common/logger"
)
// Service handles virtual tape library (MHVTL) operations
type Service struct {
db *database.DB
logger *logger.Logger
}
// NewService creates a new VTL service
func NewService(db *database.DB, log *logger.Logger) *Service {
return &Service{
db: db,
logger: log,
}
}
// VirtualTapeLibrary represents a virtual tape library
type VirtualTapeLibrary struct {
ID string `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
MHVTLibraryID int `json:"mhvtl_library_id"`
BackingStorePath string `json:"backing_store_path"`
Vendor string `json:"vendor,omitempty"`
SlotCount int `json:"slot_count"`
DriveCount int `json:"drive_count"`
IsActive bool `json:"is_active"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
CreatedBy string `json:"created_by"`
}
// VirtualTapeDrive represents a virtual tape drive
type VirtualTapeDrive struct {
ID string `json:"id"`
LibraryID string `json:"library_id"`
DriveNumber int `json:"drive_number"`
DevicePath *string `json:"device_path,omitempty"`
StablePath *string `json:"stable_path,omitempty"`
Status string `json:"status"`
CurrentTapeID string `json:"current_tape_id,omitempty"`
IsActive bool `json:"is_active"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// VirtualTape represents a virtual tape
type VirtualTape struct {
ID string `json:"id"`
LibraryID string `json:"library_id"`
Barcode string `json:"barcode"`
SlotNumber int `json:"slot_number"`
ImageFilePath string `json:"image_file_path"`
SizeBytes int64 `json:"size_bytes"`
UsedBytes int64 `json:"used_bytes"`
TapeType string `json:"tape_type"`
Status string `json:"status"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// CreateLibrary creates a new virtual tape library
func (s *Service) CreateLibrary(ctx context.Context, name, description, backingStorePath string, slotCount, driveCount int, createdBy string) (*VirtualTapeLibrary, error) {
// Ensure backing store directory exists
fullPath := filepath.Join(backingStorePath, name)
if err := os.MkdirAll(fullPath, 0755); err != nil {
return nil, fmt.Errorf("failed to create backing store directory: %w", err)
}
// Create tapes directory
tapesPath := filepath.Join(fullPath, "tapes")
if err := os.MkdirAll(tapesPath, 0755); err != nil {
return nil, fmt.Errorf("failed to create tapes directory: %w", err)
}
// Generate MHVTL library ID (use next available ID)
mhvtlID, err := s.getNextMHVTLID(ctx)
if err != nil {
return nil, fmt.Errorf("failed to get next MHVTL ID: %w", err)
}
// Insert into database
query := `
INSERT INTO virtual_tape_libraries (
name, description, mhvtl_library_id, backing_store_path,
slot_count, drive_count, is_active, created_by
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
RETURNING id, created_at, updated_at
`
var lib VirtualTapeLibrary
err = s.db.QueryRowContext(ctx, query,
name, description, mhvtlID, fullPath,
slotCount, driveCount, true, createdBy,
).Scan(&lib.ID, &lib.CreatedAt, &lib.UpdatedAt)
if err != nil {
return nil, fmt.Errorf("failed to save library to database: %w", err)
}
lib.Name = name
lib.Description = description
lib.MHVTLibraryID = mhvtlID
lib.BackingStorePath = fullPath
lib.SlotCount = slotCount
lib.DriveCount = driveCount
lib.IsActive = true
lib.CreatedBy = createdBy
// Create virtual drives
for i := 1; i <= driveCount; i++ {
drive := VirtualTapeDrive{
LibraryID: lib.ID,
DriveNumber: i,
Status: "idle",
IsActive: true,
}
if err := s.createDrive(ctx, &drive); err != nil {
s.logger.Error("Failed to create drive", "drive_number", i, "error", err)
// Continue creating other drives even if one fails
}
}
// Create initial tapes in slots
for i := 1; i <= slotCount; i++ {
barcode := fmt.Sprintf("V%05d", i)
tape := VirtualTape{
LibraryID: lib.ID,
Barcode: barcode,
SlotNumber: i,
ImageFilePath: filepath.Join(tapesPath, fmt.Sprintf("%s.img", barcode)),
SizeBytes: 800 * 1024 * 1024 * 1024, // 800 GB default (LTO-8)
UsedBytes: 0,
TapeType: "LTO-8",
Status: "idle",
}
if err := s.createTape(ctx, &tape); err != nil {
s.logger.Error("Failed to create tape", "slot", i, "error", err)
// Continue creating other tapes even if one fails
}
}
s.logger.Info("Virtual tape library created", "name", name, "id", lib.ID)
return &lib, nil
}
// getNextMHVTLID gets the next available MHVTL library ID
func (s *Service) getNextMHVTLID(ctx context.Context) (int, error) {
var maxID sql.NullInt64
err := s.db.QueryRowContext(ctx,
"SELECT MAX(mhvtl_library_id) FROM virtual_tape_libraries",
).Scan(&maxID)
if err != nil && err != sql.ErrNoRows {
return 0, err
}
if maxID.Valid {
return int(maxID.Int64) + 1, nil
}
return 1, nil
}
// createDrive creates a virtual tape drive
func (s *Service) createDrive(ctx context.Context, drive *VirtualTapeDrive) error {
query := `
INSERT INTO virtual_tape_drives (
library_id, drive_number, status, is_active
) VALUES ($1, $2, $3, $4)
RETURNING id, created_at, updated_at
`
err := s.db.QueryRowContext(ctx, query,
drive.LibraryID, drive.DriveNumber, drive.Status, drive.IsActive,
).Scan(&drive.ID, &drive.CreatedAt, &drive.UpdatedAt)
if err != nil {
return fmt.Errorf("failed to create drive: %w", err)
}
return nil
}
// createTape creates a virtual tape
func (s *Service) createTape(ctx context.Context, tape *VirtualTape) error {
// Create empty tape image file
file, err := os.Create(tape.ImageFilePath)
if err != nil {
return fmt.Errorf("failed to create tape image: %w", err)
}
file.Close()
query := `
INSERT INTO virtual_tapes (
library_id, barcode, slot_number, image_file_path,
size_bytes, used_bytes, tape_type, status
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
RETURNING id, created_at, updated_at
`
err = s.db.QueryRowContext(ctx, query,
tape.LibraryID, tape.Barcode, tape.SlotNumber, tape.ImageFilePath,
tape.SizeBytes, tape.UsedBytes, tape.TapeType, tape.Status,
).Scan(&tape.ID, &tape.CreatedAt, &tape.UpdatedAt)
if err != nil {
return fmt.Errorf("failed to create tape: %w", err)
}
return nil
}
// ListLibraries lists all virtual tape libraries
func (s *Service) ListLibraries(ctx context.Context) ([]VirtualTapeLibrary, error) {
query := `
SELECT id, name, description, mhvtl_library_id, backing_store_path,
COALESCE(vendor, '') as vendor,
slot_count, drive_count, is_active, created_at, updated_at, created_by
FROM virtual_tape_libraries
ORDER BY name
`
s.logger.Info("Executing query to list libraries")
rows, err := s.db.QueryContext(ctx, query)
if err != nil {
s.logger.Error("Failed to query libraries", "error", err)
return nil, fmt.Errorf("failed to list libraries: %w", err)
}
s.logger.Info("Query executed successfully, got rows")
defer rows.Close()
libraries := make([]VirtualTapeLibrary, 0) // Initialize as empty slice, not nil
s.logger.Info("Starting to scan library rows", "query", query)
rowCount := 0
for rows.Next() {
rowCount++
var lib VirtualTapeLibrary
var description sql.NullString
var createdBy sql.NullString
err := rows.Scan(
&lib.ID, &lib.Name, &description, &lib.MHVTLibraryID, &lib.BackingStorePath,
&lib.Vendor,
&lib.SlotCount, &lib.DriveCount, &lib.IsActive,
&lib.CreatedAt, &lib.UpdatedAt, &createdBy,
)
if err != nil {
s.logger.Error("Failed to scan library", "error", err, "row", rowCount)
continue
}
if description.Valid {
lib.Description = description.String
}
if createdBy.Valid {
lib.CreatedBy = createdBy.String
}
libraries = append(libraries, lib)
s.logger.Info("Added library to list", "library_id", lib.ID, "name", lib.Name, "mhvtl_id", lib.MHVTLibraryID)
}
s.logger.Info("Finished scanning library rows", "total_rows", rowCount, "libraries_added", len(libraries))
if err := rows.Err(); err != nil {
s.logger.Error("Error iterating library rows", "error", err)
return nil, fmt.Errorf("error iterating library rows: %w", err)
}
s.logger.Info("Listed virtual tape libraries", "count", len(libraries), "is_nil", libraries == nil)
// Ensure we return an empty slice, not nil
if libraries == nil {
s.logger.Warn("Libraries is nil in service, converting to empty array")
libraries = []VirtualTapeLibrary{}
}
s.logger.Info("Returning from service", "count", len(libraries), "is_nil", libraries == nil)
return libraries, nil
}
// GetLibrary retrieves a library by ID
func (s *Service) GetLibrary(ctx context.Context, id string) (*VirtualTapeLibrary, error) {
query := `
SELECT id, name, description, mhvtl_library_id, backing_store_path,
COALESCE(vendor, '') as vendor,
slot_count, drive_count, is_active, created_at, updated_at, created_by
FROM virtual_tape_libraries
WHERE id = $1
`
var lib VirtualTapeLibrary
var description sql.NullString
var createdBy sql.NullString
err := s.db.QueryRowContext(ctx, query, id).Scan(
&lib.ID, &lib.Name, &description, &lib.MHVTLibraryID, &lib.BackingStorePath,
&lib.Vendor,
&lib.SlotCount, &lib.DriveCount, &lib.IsActive,
&lib.CreatedAt, &lib.UpdatedAt, &createdBy,
)
if err != nil {
if err == sql.ErrNoRows {
return nil, fmt.Errorf("library not found")
}
return nil, fmt.Errorf("failed to get library: %w", err)
}
if description.Valid {
lib.Description = description.String
}
if createdBy.Valid {
lib.CreatedBy = createdBy.String
}
return &lib, nil
}
// GetLibraryDrives retrieves drives for a library
func (s *Service) GetLibraryDrives(ctx context.Context, libraryID string) ([]VirtualTapeDrive, error) {
query := `
SELECT id, library_id, drive_number, device_path, stable_path,
status, current_tape_id, is_active, created_at, updated_at
FROM virtual_tape_drives
WHERE library_id = $1
ORDER BY drive_number
`
rows, err := s.db.QueryContext(ctx, query, libraryID)
if err != nil {
return nil, fmt.Errorf("failed to get drives: %w", err)
}
defer rows.Close()
var drives []VirtualTapeDrive
for rows.Next() {
var drive VirtualTapeDrive
var tapeID, devicePath, stablePath sql.NullString
err := rows.Scan(
&drive.ID, &drive.LibraryID, &drive.DriveNumber,
&devicePath, &stablePath,
&drive.Status, &tapeID, &drive.IsActive,
&drive.CreatedAt, &drive.UpdatedAt,
)
if err != nil {
s.logger.Error("Failed to scan drive", "error", err)
continue
}
if devicePath.Valid {
drive.DevicePath = &devicePath.String
}
if stablePath.Valid {
drive.StablePath = &stablePath.String
}
if tapeID.Valid {
drive.CurrentTapeID = tapeID.String
}
drives = append(drives, drive)
}
return drives, rows.Err()
}
// GetLibraryTapes retrieves tapes for a library
func (s *Service) GetLibraryTapes(ctx context.Context, libraryID string) ([]VirtualTape, error) {
query := `
SELECT id, library_id, barcode, slot_number, image_file_path,
size_bytes, used_bytes, tape_type, status, created_at, updated_at
FROM virtual_tapes
WHERE library_id = $1
ORDER BY slot_number
`
rows, err := s.db.QueryContext(ctx, query, libraryID)
if err != nil {
return nil, fmt.Errorf("failed to get tapes: %w", err)
}
defer rows.Close()
var tapes []VirtualTape
for rows.Next() {
var tape VirtualTape
err := rows.Scan(
&tape.ID, &tape.LibraryID, &tape.Barcode, &tape.SlotNumber,
&tape.ImageFilePath, &tape.SizeBytes, &tape.UsedBytes,
&tape.TapeType, &tape.Status, &tape.CreatedAt, &tape.UpdatedAt,
)
if err != nil {
s.logger.Error("Failed to scan tape", "error", err)
continue
}
tapes = append(tapes, tape)
}
return tapes, rows.Err()
}
// CreateTape creates a new virtual tape
func (s *Service) CreateTape(ctx context.Context, libraryID, barcode string, slotNumber int, tapeType string, sizeBytes int64) (*VirtualTape, error) {
// Get library to find backing store path
lib, err := s.GetLibrary(ctx, libraryID)
if err != nil {
return nil, err
}
// Create tape image file
tapesPath := filepath.Join(lib.BackingStorePath, "tapes")
imagePath := filepath.Join(tapesPath, fmt.Sprintf("%s.img", barcode))
file, err := os.Create(imagePath)
if err != nil {
return nil, fmt.Errorf("failed to create tape image: %w", err)
}
file.Close()
tape := VirtualTape{
LibraryID: libraryID,
Barcode: barcode,
SlotNumber: slotNumber,
ImageFilePath: imagePath,
SizeBytes: sizeBytes,
UsedBytes: 0,
TapeType: tapeType,
Status: "idle",
}
return s.createTapeRecord(ctx, &tape)
}
// createTapeRecord creates a tape record in the database
func (s *Service) createTapeRecord(ctx context.Context, tape *VirtualTape) (*VirtualTape, error) {
query := `
INSERT INTO virtual_tapes (
library_id, barcode, slot_number, image_file_path,
size_bytes, used_bytes, tape_type, status
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
RETURNING id, created_at, updated_at
`
err := s.db.QueryRowContext(ctx, query,
tape.LibraryID, tape.Barcode, tape.SlotNumber, tape.ImageFilePath,
tape.SizeBytes, tape.UsedBytes, tape.TapeType, tape.Status,
).Scan(&tape.ID, &tape.CreatedAt, &tape.UpdatedAt)
if err != nil {
return nil, fmt.Errorf("failed to create tape record: %w", err)
}
return tape, nil
}
// LoadTape loads a tape from slot to drive
func (s *Service) LoadTape(ctx context.Context, libraryID string, slotNumber, driveNumber int) error {
// Get tape from slot
var tapeID, barcode string
err := s.db.QueryRowContext(ctx,
"SELECT id, barcode FROM virtual_tapes WHERE library_id = $1 AND slot_number = $2",
libraryID, slotNumber,
).Scan(&tapeID, &barcode)
if err != nil {
return fmt.Errorf("tape not found in slot: %w", err)
}
// Update tape status
_, err = s.db.ExecContext(ctx,
"UPDATE virtual_tapes SET status = 'in_drive', updated_at = NOW() WHERE id = $1",
tapeID,
)
if err != nil {
return fmt.Errorf("failed to update tape status: %w", err)
}
// Update drive status
_, err = s.db.ExecContext(ctx,
"UPDATE virtual_tape_drives SET status = 'ready', current_tape_id = $1, updated_at = NOW() WHERE library_id = $2 AND drive_number = $3",
tapeID, libraryID, driveNumber,
)
if err != nil {
return fmt.Errorf("failed to update drive status: %w", err)
}
s.logger.Info("Virtual tape loaded", "library_id", libraryID, "slot", slotNumber, "drive", driveNumber, "barcode", barcode)
return nil
}
// UnloadTape unloads a tape from drive to slot
func (s *Service) UnloadTape(ctx context.Context, libraryID string, driveNumber, slotNumber int) error {
// Get current tape in drive
var tapeID string
err := s.db.QueryRowContext(ctx,
"SELECT current_tape_id FROM virtual_tape_drives WHERE library_id = $1 AND drive_number = $2",
libraryID, driveNumber,
).Scan(&tapeID)
if err != nil {
return fmt.Errorf("no tape in drive: %w", err)
}
// Update tape status and slot
_, err = s.db.ExecContext(ctx,
"UPDATE virtual_tapes SET status = 'idle', slot_number = $1, updated_at = NOW() WHERE id = $2",
slotNumber, tapeID,
)
if err != nil {
return fmt.Errorf("failed to update tape: %w", err)
}
// Update drive status
_, err = s.db.ExecContext(ctx,
"UPDATE virtual_tape_drives SET status = 'idle', current_tape_id = NULL, updated_at = NOW() WHERE library_id = $1 AND drive_number = $2",
libraryID, driveNumber,
)
if err != nil {
return fmt.Errorf("failed to update drive: %w", err)
}
s.logger.Info("Virtual tape unloaded", "library_id", libraryID, "drive", driveNumber, "slot", slotNumber)
return nil
}
// DeleteLibrary deletes a virtual tape library
func (s *Service) DeleteLibrary(ctx context.Context, id string) error {
lib, err := s.GetLibrary(ctx, id)
if err != nil {
return err
}
if lib.IsActive {
return fmt.Errorf("cannot delete active library")
}
// Delete from database (cascade will handle drives and tapes)
_, err = s.db.ExecContext(ctx, "DELETE FROM virtual_tape_libraries WHERE id = $1", id)
if err != nil {
return fmt.Errorf("failed to delete library: %w", err)
}
// Optionally remove backing store (commented out for safety)
// os.RemoveAll(lib.BackingStorePath)
s.logger.Info("Virtual tape library deleted", "id", id, "name", lib.Name)
return nil
}