545 lines
16 KiB
Go
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
|
|
}
|