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"` 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, 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.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, 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.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 }