package services import ( "bufio" "fmt" "log" "os" "os/exec" "path/filepath" "regexp" "strconv" "strings" "gitea.avt.data-center.id/othman.suseno/atlas/internal/models" ) // DeviceConfig represents parsed device.conf structure type DeviceConfig struct { Libraries []LibraryConfig Drives []DriveConfig } // LibraryConfig represents a library from device.conf type LibraryConfig struct { ID int Vendor string Product string Serial string } // DriveConfig represents a drive from device.conf type DriveConfig struct { ID int LibraryID int SlotID int Vendor string Product string Serial string } // VTLService manages mhvtl (Virtual Tape Library) type VTLService struct { configPath string // Path to mhvtl config (default: /etc/mhvtl/mhvtl.conf) deviceConfigPath string // Path to device config (default: /etc/mhvtl/device.conf) storagePath string // Path to tape storage (default: /opt/mhvtl) } // NewVTLService creates a new VTL service func NewVTLService() *VTLService { return &VTLService{ configPath: "/etc/mhvtl/mhvtl.conf", deviceConfigPath: "/etc/mhvtl/device.conf", storagePath: "/opt/mhvtl", } } // GetStatus returns the status of mhvtl service func (s *VTLService) GetStatus() (*models.VTLStatus, error) { status := &models.VTLStatus{ ServiceRunning: false, DrivesOnline: 0, DrivesTotal: 0, TapesTotal: 0, TapesAvailable: 0, } // Check if mhvtl target is active (mhvtl uses mhvtl.target, not mhvtl.service) cmd := exec.Command("systemctl", "is-active", "mhvtl.target") if err := cmd.Run(); err == nil { status.ServiceRunning = true } else { // Fallback: check if mhvtl-load-modules is active (modules loaded) cmd2 := exec.Command("systemctl", "is-active", "mhvtl-load-modules.service") if cmd2.Run() == nil { // Modules are loaded, service might be running even if target shows inactive status.ServiceRunning = true } } // Get drives and tapes info drives, err := s.ListDrives() if err == nil { status.DrivesTotal = len(drives) for _, drive := range drives { if drive.Status == "online" { status.DrivesOnline++ } } } tapes, err := s.ListTapes() if err == nil { status.TapesTotal = len(tapes) for _, tape := range tapes { if tape.Status == "available" { status.TapesAvailable++ } } } return status, nil } // ListDrives lists all virtual tape drives from device.conf func (s *VTLService) ListDrives() ([]models.VTLDrive, error) { drives := []models.VTLDrive{} // Read from device.conf first (more accurate) deviceConfig, err := s.parseDeviceConfig() if err == nil && len(deviceConfig.Drives) > 0 { // Use drives from device.conf for _, driveConfig := range deviceConfig.Drives { // Find corresponding device from sysfs devicePath := s.findDeviceForDrive(driveConfig.ID) // Check if media is loaded mediaLoaded, barcode := s.checkMediaLoadedByDriveID(driveConfig.ID) drive := models.VTLDrive{ ID: driveConfig.ID, LibraryID: driveConfig.LibraryID, SlotID: driveConfig.SlotID, Vendor: driveConfig.Vendor, Product: driveConfig.Product, Type: s.determineDriveType(driveConfig.Product), Device: devicePath, Status: "online", MediaLoaded: mediaLoaded, Barcode: barcode, } drives = append(drives, drive) } return drives, nil } // Fallback: Read from /sys/class/scsi_tape/ if device.conf not available tapePath := "/sys/class/scsi_tape" entries, err := os.ReadDir(tapePath) if err != nil { return drives, nil } for _, entry := range entries { if !entry.IsDir() { continue } deviceName := entry.Name() if !strings.HasPrefix(deviceName, "st") && !strings.HasPrefix(deviceName, "nst") { continue } devicePath := fmt.Sprintf("/dev/%s", deviceName) vendorPath := fmt.Sprintf("%s/%s/device/vendor", tapePath, deviceName) productPath := fmt.Sprintf("%s/%s/device/model", tapePath, deviceName) vendor := "Unknown" product := "Unknown" if vendorBytes, err := os.ReadFile(vendorPath); err == nil { vendor = strings.TrimSpace(string(vendorBytes)) } if productBytes, err := os.ReadFile(productPath); err == nil { product = strings.TrimSpace(string(productBytes)) } driveID := s.getDriveIDFromDevice(deviceName) mediaLoaded, barcode := s.checkMediaLoaded(deviceName) // Try to get library ID from device.conf if available libraryID := driveID / 10 slotID := driveID % 10 // Try to get more accurate info from device.conf deviceConfig, err := s.parseDeviceConfig() if err == nil { for _, driveConfig := range deviceConfig.Drives { if driveConfig.ID == driveID { libraryID = driveConfig.LibraryID slotID = driveConfig.SlotID if vendor == "Unknown" { vendor = driveConfig.Vendor } if product == "Unknown" { product = driveConfig.Product } break } } } drive := models.VTLDrive{ ID: driveID, LibraryID: libraryID, SlotID: slotID, Vendor: vendor, Product: product, Type: s.determineDriveType(product), Device: devicePath, Status: "online", MediaLoaded: mediaLoaded, Barcode: barcode, } drives = append(drives, drive) } return drives, nil } // ListTapes lists all virtual tapes from library_contents and storage directory func (s *VTLService) ListTapes() ([]models.VTLTape, error) { tapes := []models.VTLTape{} // Parse device config to get library IDs deviceConfig, err := s.parseDeviceConfig() if err != nil { log.Printf("Warning: failed to parse device.conf: %v", err) } // Map to track which tapes are in which slots slotToBarcode := make(map[string]string) // "libraryID:slotID" -> barcode // Read library_contents files for each library libraryIDs := []int{10, 30} // Default libraries, or get from deviceConfig if deviceConfig != nil && len(deviceConfig.Libraries) > 0 { libraryIDs = []int{} for _, lib := range deviceConfig.Libraries { libraryIDs = append(libraryIDs, lib.ID) } } for _, libID := range libraryIDs { slotMap, err := s.parseLibraryContents(libID) if err == nil { for slotID, barcode := range slotMap { key := fmt.Sprintf("%d:%d", libID, slotID) slotToBarcode[key] = barcode } } } // Read tapes from storage directory // mhvtl stores tapes directly in /opt/mhvtl/, not in /opt/mhvtl/data/ tapeStoragePath := s.storagePath entries, err := os.ReadDir(tapeStoragePath) if err != nil { // No tapes directory yet log.Printf("Warning: cannot read tape storage directory %s: %v", tapeStoragePath, err) return tapes, nil } // Create a map of barcodes to their slot info barcodeToSlot := make(map[string]struct { LibraryID int SlotID int }) for key, barcode := range slotToBarcode { parts := strings.Split(key, ":") if len(parts) == 2 { libID, _ := strconv.Atoi(parts[0]) slotID, _ := strconv.Atoi(parts[1]) barcodeToSlot[barcode] = struct { LibraryID int SlotID int }{LibraryID: libID, SlotID: slotID} } } for _, entry := range entries { if entry.IsDir() { barcode := entry.Name() tapePath := filepath.Join(tapeStoragePath, barcode) // Get slot info from library_contents slotInfo, inSlot := barcodeToSlot[barcode] libraryID := 0 slotID := 0 if inSlot { libraryID = slotInfo.LibraryID slotID = slotInfo.SlotID } else { // If not in library_contents, try to find from device.conf libraries // Default to first library if available if deviceConfig != nil && len(deviceConfig.Libraries) > 0 { libraryID = deviceConfig.Libraries[0].ID } else { libraryID = 10 // Fallback default } } // Determine tape type from barcode suffix tapeType := s.determineTapeTypeFromBarcode(barcode) // Get default size based on tape generation defaultSize := s.getDefaultTapeSize(tapeType) tape := models.VTLTape{ Barcode: barcode, LibraryID: libraryID, SlotID: slotID, DriveID: -1, // Not loaded (would need to check drive status) Type: tapeType, Size: defaultSize, // Use default size for tape generation Used: 0, Status: "available", } // Get actual used space from tape data file (mhvtl uses "data" file, not "tape") tapeFile := filepath.Join(tapePath, "data") if info, err := os.Stat(tapeFile); err == nil { // Used space is the actual file size tape.Used = uint64(info.Size()) } tapes = append(tapes, tape) } } return tapes, nil } // CreateTape creates a new virtual tape // libraryID: Library ID where the tape will be placed // slotID: Slot ID in the library where the tape will be placed func (s *VTLService) CreateTape(barcode string, tapeType string, size uint64, libraryID int, slotID int) error { // Validate barcode format (must follow LTO standard) if err := s.validateLTOBarcode(barcode); err != nil { return fmt.Errorf("invalid barcode format: %v", err) } // Determine tape type from barcode if not provided if tapeType == "" { tapeType = s.determineTapeTypeFromBarcode(barcode) } // Use default size based on tape generation if not specified if size == 0 { size = s.getDefaultTapeSize(tapeType) } // Validate library ID and slot ID if libraryID <= 0 { return fmt.Errorf("library ID must be greater than 0") } if slotID <= 0 { return fmt.Errorf("slot ID must be greater than 0") } // Check if slot is already occupied slotMap, err := s.parseLibraryContents(libraryID) if err == nil { if existingBarcode, occupied := slotMap[slotID]; occupied && existingBarcode != "" && existingBarcode != "-" { return fmt.Errorf("slot %d in library %d is already occupied by tape %s", slotID, libraryID, existingBarcode) } } // Check if tape already exists tapePath := filepath.Join(s.storagePath, barcode) log.Printf("Creating tape: barcode=%s, path=%s, library=%d, slot=%d", barcode, tapePath, libraryID, slotID) if _, err := os.Stat(tapePath); err == nil { return fmt.Errorf("tape %s already exists at %s", barcode, tapePath) } // Ensure parent directory exists first parentDir := s.storagePath log.Printf("Checking parent directory: %s", parentDir) // Check if filesystem is read-only and try to remount if err := s.ensureWritableFilesystem(parentDir); err != nil { log.Printf("Warning: failed to ensure writable filesystem: %v", err) // Continue anyway, might still work } if _, err := os.Stat(parentDir); os.IsNotExist(err) { log.Printf("Parent directory does not exist, creating: %s", parentDir) // Try to create parent directory if err := os.MkdirAll(parentDir, 0755); err != nil { log.Printf("Regular mkdir failed for parent %s: %v, trying with sudo", parentDir, err) // Try with sudo if regular mkdir fails cmd := exec.Command("sudo", "mkdir", "-p", parentDir) if output, err := cmd.CombinedOutput(); err != nil { outputStr := strings.TrimSpace(string(output)) return fmt.Errorf("failed to create parent directory %s: %v, output: %s", parentDir, err, outputStr) } // Set permissions cmd = exec.Command("sudo", "chmod", "755", parentDir) cmd.Run() // Ignore error for chmod } } else { // Check if we can write to parent directory if testFile, err := os.CreateTemp(parentDir, ".write-test-*"); err != nil { log.Printf("Warning: cannot write to parent directory %s: %v, attempting remount", parentDir, err) // Try to remount as read-write if remountErr := s.remountReadWrite(parentDir); remountErr != nil { log.Printf("Warning: remount failed: %v", remountErr) } } else { testFile.Close() os.Remove(testFile.Name()) log.Printf("Parent directory is writable: %s", parentDir) } } // Check if tape directory already exists (should have been caught earlier, but double-check) if _, err := os.Stat(tapePath); err == nil { // Directory exists, check if it's empty or has files entries, _ := os.ReadDir(tapePath) if len(entries) > 0 { return fmt.Errorf("tape directory %s already exists and is not empty", tapePath) } // Directory exists but empty, we can use it } else { // Create tape directory (may need root permissions for /opt/mhvtl) // Try without sudo first, if fails, try with sudo log.Printf("Attempting to create directory: %s", tapePath) if err := os.MkdirAll(tapePath, 0750); err != nil { log.Printf("Regular mkdir failed for %s: %v, trying with sudo", tapePath, err) // Try with sudo if regular mkdir fails cmd := exec.Command("sudo", "mkdir", "-p", tapePath) if output, err := cmd.CombinedOutput(); err != nil { outputStr := strings.TrimSpace(string(output)) log.Printf("Sudo mkdir failed for %s: %v, output: %s", tapePath, err, outputStr) return fmt.Errorf("failed to create tape directory %s: %v (output: %s)", tapePath, err, outputStr) } log.Printf("Directory created with sudo: %s", tapePath) // Set permissions after creating with sudo cmd = exec.Command("sudo", "chmod", "750", tapePath) if output, err := cmd.CombinedOutput(); err != nil { log.Printf("Warning: failed to set permissions on %s: %v, output: %s", tapePath, err, string(output)) } } else { log.Printf("Directory created successfully: %s", tapePath) } } // Create tape data file (mhvtl uses "data" file, not "tape") tapeFile := filepath.Join(tapePath, "data") file, err := os.Create(tapeFile) fileCreated := true if err != nil { // Try with sudo if regular create fails cmd := exec.Command("sudo", "touch", tapeFile) if output, err := cmd.CombinedOutput(); err != nil { return fmt.Errorf("failed to create tape file %s: %v, output: %s", tapeFile, err, string(output)) } // Set permissions cmd = exec.Command("sudo", "chmod", "644", tapeFile) cmd.Run() // Ignore error file, err = os.OpenFile(tapeFile, os.O_WRONLY, 0644) if err != nil { return fmt.Errorf("failed to open tape file %s: %v", tapeFile, err) } fileCreated = false } // Pre-allocate space if size is specified (do this before closing file) if size > 0 { if fileCreated { // File was created by us, we can truncate it directly if err := file.Truncate(int64(size)); err != nil { log.Printf("Warning: failed to truncate tape file: %v, trying with sudo", err) file.Close() // Try with sudo if truncate fails cmd := exec.Command("sudo", "truncate", "-s", fmt.Sprintf("%d", size), tapeFile) if output, err := cmd.CombinedOutput(); err != nil { log.Printf("Warning: failed to pre-allocate tape space: %v, output: %s", err, string(output)) } else { log.Printf("Tape file truncated to %d bytes using sudo", size) } } else { log.Printf("Tape file truncated to %d bytes", size) } } else { // File was created with sudo, use sudo truncate file.Close() cmd := exec.Command("sudo", "truncate", "-s", fmt.Sprintf("%d", size), tapeFile) if output, err := cmd.CombinedOutput(); err != nil { log.Printf("Warning: failed to pre-allocate tape space: %v, output: %s", err, string(output)) } else { log.Printf("Tape file truncated to %d bytes using sudo", size) } } } if fileCreated { file.Close() } // Create index file (mhvtl also uses "indx" file) indxFile := filepath.Join(tapePath, "indx") indx, err := os.Create(indxFile) if err != nil { // Try with sudo if regular create fails cmd := exec.Command("sudo", "touch", indxFile) if output, err := cmd.CombinedOutput(); err != nil { log.Printf("Warning: failed to create index file %s: %v, output: %s", indxFile, err, string(output)) } else { // Set permissions cmd = exec.Command("sudo", "chmod", "644", indxFile) if output, err := cmd.CombinedOutput(); err != nil { log.Printf("Warning: failed to set permissions on %s: %v, output: %s", indxFile, err, string(output)) } } } else { indx.Close() } // Add tape to library_contents file if err := s.addTapeToLibraryContents(libraryID, slotID, barcode); err != nil { // Clean up created directory if library_contents update fails os.RemoveAll(tapePath) return fmt.Errorf("failed to add tape to library_contents: %v", err) } // Restart mhvtl service to reflect changes if err := s.RestartService(); err != nil { log.Printf("Warning: failed to restart mhvtl service after tape creation: %v", err) // Continue even if restart fails - tape is created } log.Printf("Created virtual tape: %s (type: %s, size: %d bytes, library: %d, slot: %d)", barcode, tapeType, size, libraryID, slotID) return nil } // DeleteTape deletes a virtual tape func (s *VTLService) DeleteTape(barcode string) error { // mhvtl stores tapes directly in /opt/mhvtl/, not in /opt/mhvtl/data/ tapePath := filepath.Join(s.storagePath, barcode) log.Printf("Deleting tape: barcode=%s, path=%s", barcode, tapePath) // Check if tape directory exists if _, err := os.Stat(tapePath); os.IsNotExist(err) { return fmt.Errorf("tape %s does not exist", barcode) } // Ensure filesystem is writable before delete (same as create) parentDir := s.storagePath if err := s.ensureWritableFilesystem(parentDir); err != nil { log.Printf("Warning: failed to ensure writable filesystem: %v", err) // Continue anyway, might still work } // Find which library and slot this tape is in libraryID, slotID := s.findTapeInLibrary(barcode) log.Printf("Tape %s found in library %d, slot %d", barcode, libraryID, slotID) // Remove the entire tape directory // Try without sudo first, if fails, try with sudo if err := os.RemoveAll(tapePath); err != nil { log.Printf("Regular RemoveAll failed for %s: %v, trying with sudo", tapePath, err) // Try with sudo if regular remove fails cmd := exec.Command("sudo", "rm", "-rf", tapePath) if output, err := cmd.CombinedOutput(); err != nil { outputStr := strings.TrimSpace(string(output)) return fmt.Errorf("failed to delete tape directory %s: %v, output: %s", tapePath, err, outputStr) } log.Printf("Tape directory deleted with sudo: %s", tapePath) } else { log.Printf("Tape directory deleted successfully: %s", tapePath) } // Remove from library_contents if found if libraryID > 0 && slotID > 0 { if err := s.removeTapeFromLibraryContents(libraryID, slotID); err != nil { log.Printf("Warning: failed to remove tape from library_contents.%d: %v", libraryID, err) // Continue even if library_contents update fails } } // Restart mhvtl service to reflect changes if err := s.RestartService(); err != nil { log.Printf("Warning: failed to restart mhvtl service after tape deletion: %v", err) // Continue even if restart fails } log.Printf("Deleted virtual tape: %s (path: %s, library: %d, slot: %d)", barcode, tapePath, libraryID, slotID) return nil } // StartService starts the mhvtl service func (s *VTLService) StartService() error { // Check if already active first checkCmd := exec.Command("systemctl", "is-active", "mhvtl.target") if err := checkCmd.Run(); err == nil { log.Printf("mhvtl.target is already active") return nil // Already running, no error } // mhvtl uses mhvtl.target, not mhvtl.service // First, ensure modules are loaded cmd := exec.Command("systemctl", "start", "mhvtl-load-modules.service") if err := cmd.Run(); err != nil { // Ignore error if modules are already loaded log.Printf("Warning: mhvtl-load-modules start returned: %v (may already be loaded)", err) } // Start mhvtl.target (this starts all mhvtl services) cmd = exec.Command("systemctl", "start", "mhvtl.target") output, err := cmd.CombinedOutput() if err != nil { // Check exit code for better error message if exitError, ok := err.(*exec.ExitError); ok { exitCode := exitError.ExitCode() outputStr := strings.TrimSpace(string(output)) if outputStr == "" { outputStr = "no output" } log.Printf("mhvtl.target start failed: exit code %d, output: %s", exitCode, outputStr) // Provide specific error messages based on exit code switch exitCode { case 1: return fmt.Errorf("failed to start mhvtl service: unit failed to start. Check logs: journalctl -u mhvtl.target -n 50") case 5: return fmt.Errorf("failed to start mhvtl service: unit not found or permission denied. Ensure mhvtl is installed: apt-get install mhvtl") default: return fmt.Errorf("failed to start mhvtl service: exit code %d, output: %s", exitCode, outputStr) } } return fmt.Errorf("failed to start mhvtl service: %v", err) } log.Printf("mhvtl.target started successfully") return nil } // StopService stops the mhvtl service func (s *VTLService) StopService() error { // Check if already inactive first checkCmd := exec.Command("systemctl", "is-active", "mhvtl.target") if err := checkCmd.Run(); err != nil { log.Printf("mhvtl.target is already inactive") return nil // Already stopped, no error } // Stop mhvtl.target (this stops all mhvtl services) cmd := exec.Command("systemctl", "stop", "mhvtl.target") output, err := cmd.CombinedOutput() if err != nil { if exitError, ok := err.(*exec.ExitError); ok { exitCode := exitError.ExitCode() outputStr := strings.TrimSpace(string(output)) if outputStr == "" { outputStr = "no output" } log.Printf("mhvtl.target stop failed: exit code %d, output: %s", exitCode, outputStr) return fmt.Errorf("failed to stop mhvtl service: exit code %d, output: %s", exitCode, outputStr) } return fmt.Errorf("failed to stop mhvtl service: %v", err) } log.Printf("mhvtl.target stopped successfully") return nil } // RestartService restarts the mhvtl service func (s *VTLService) RestartService() error { // Restart mhvtl.target cmd := exec.Command("systemctl", "restart", "mhvtl.target") output, err := cmd.CombinedOutput() if err != nil { if exitError, ok := err.(*exec.ExitError); ok { exitCode := exitError.ExitCode() outputStr := string(output) if outputStr == "" { outputStr = "no output" } log.Printf("mhvtl.target restart failed: exit code %d, output: %s", exitCode, outputStr) switch exitCode { case 1: return fmt.Errorf("failed to restart mhvtl service: unit failed to restart (check logs: journalctl -u mhvtl.target)") case 5: return fmt.Errorf("failed to restart mhvtl service: unit not found or permission denied") default: return fmt.Errorf("failed to restart mhvtl service: exit code %d, output: %s", exitCode, outputStr) } } return fmt.Errorf("failed to restart mhvtl service: %v", err) } log.Printf("mhvtl.target restarted successfully") return nil } // Helper functions func (s *VTLService) determineDriveType(product string) string { product = strings.ToUpper(product) if strings.Contains(product, "LTO-9") || strings.Contains(product, "TD9") { return "LTO-9" } if strings.Contains(product, "LTO-8") || strings.Contains(product, "TD8") { return "LTO-8" } if strings.Contains(product, "LTO-7") || strings.Contains(product, "TD7") { return "LTO-7" } if strings.Contains(product, "LTO-6") || strings.Contains(product, "TD6") { return "LTO-6" } if strings.Contains(product, "LTO-5") || strings.Contains(product, "TD5") { return "LTO-5" } return "Unknown" } func (s *VTLService) getDriveIDFromDevice(deviceName string) int { // Extract number from device name (e.g., "st0" -> 0, "nst1" -> 1) // For now, use a simple mapping // In real implementation, this should read from mhvtl config if strings.HasPrefix(deviceName, "nst") { numStr := strings.TrimPrefix(deviceName, "nst") if num, err := strconv.Atoi(numStr); err == nil { return num + 10 // Assume library 10 } } else if strings.HasPrefix(deviceName, "st") { numStr := strings.TrimPrefix(deviceName, "st") if num, err := strconv.Atoi(numStr); err == nil { return num + 10 // Assume library 10 } } return 0 } func (s *VTLService) checkMediaLoaded(deviceName string) (bool, string) { // Check if tape is loaded by reading from device // This is a simplified check - in real implementation, use mt or sg commands statusPath := fmt.Sprintf("/sys/class/scsi_tape/%s/device/tape_stat", deviceName) if _, err := os.Stat(statusPath); err == nil { // Tape device exists, might have media // For now, return false - real implementation should check actual status return false, "" } return false, "" } // ListMediaChangers lists all virtual media changers from device.conf func (s *VTLService) ListMediaChangers() ([]models.VTLMediaChanger, error) { changers := []models.VTLMediaChanger{} // Parse device.conf to get libraries deviceConfig, err := s.parseDeviceConfig() if err == nil && len(deviceConfig.Libraries) > 0 { // Count drives per library drivesPerLibrary := make(map[int]int) for _, drive := range deviceConfig.Drives { drivesPerLibrary[drive.LibraryID]++ } // Count slots per library from library_contents slotsPerLibrary := make(map[int]int) for _, lib := range deviceConfig.Libraries { slotMap, err := s.parseLibraryContents(lib.ID) if err == nil { slotsPerLibrary[lib.ID] = len(slotMap) } else { slotsPerLibrary[lib.ID] = 10 // Default } } // Find device path for each library for _, lib := range deviceConfig.Libraries { devicePath := s.findMediaChangerDevice(lib.ID) changer := models.VTLMediaChanger{ ID: lib.ID, LibraryID: lib.ID, Device: devicePath, Status: "online", Slots: slotsPerLibrary[lib.ID], Drives: drivesPerLibrary[lib.ID], } changers = append(changers, changer) } return changers, nil } // Fallback: Read from /sys/class/scsi_generic/ sgPath := "/sys/class/scsi_generic" entries, err := os.ReadDir(sgPath) if err != nil { return changers, nil } for _, entry := range entries { if !entry.IsDir() { continue } deviceName := entry.Name() devicePath := fmt.Sprintf("/dev/%s", deviceName) vendorPath := fmt.Sprintf("%s/%s/device/vendor", sgPath, deviceName) productPath := fmt.Sprintf("%s/%s/device/model", sgPath, deviceName) vendor := "Unknown" product := "Unknown" if vendorBytes, err := os.ReadFile(vendorPath); err == nil { vendor = strings.TrimSpace(string(vendorBytes)) } if productBytes, err := os.ReadFile(productPath); err == nil { product = strings.TrimSpace(string(productBytes)) } // Check if it's a media changer (STK L700 or L80) if strings.Contains(strings.ToUpper(vendor), "STK") || strings.Contains(strings.ToUpper(product), "L700") || strings.Contains(strings.ToUpper(product), "L80") { // Try to determine library ID from device libraryID := 10 if strings.Contains(strings.ToUpper(product), "L80") { libraryID = 30 } // Count slots and drives for this library slotMap, _ := s.parseLibraryContents(libraryID) slots := len(slotMap) if slots == 0 { slots = 10 // Default } // Count drives from device.conf deviceConfig, _ := s.parseDeviceConfig() drives := 0 if deviceConfig != nil { for _, drive := range deviceConfig.Drives { if drive.LibraryID == libraryID { drives++ } } } if drives == 0 { drives = 4 // Default } changer := models.VTLMediaChanger{ ID: libraryID, LibraryID: libraryID, Device: devicePath, Status: "online", Slots: slots, Drives: drives, } changers = append(changers, changer) } } return changers, nil } // LoadTape loads a tape into a drive using mtx (media changer command) func (s *VTLService) LoadTape(driveID int, barcode string) error { // Find the media changer device changers, err := s.ListMediaChangers() if err != nil || len(changers) == 0 { return fmt.Errorf("no media changer found") } changer := changers[0] // Find the slot containing this tape // In real implementation, this would query the media changer for slot info // For now, we'll use a simplified approach slotID := 1 // Default slot // Use mtx command to load tape: mtx -f /dev/sg0 load // Drive mapping: drive 0 = element 0, drive 1 = element 1, etc. // Slot mapping: slots start from element 256 mtxCmd := exec.Command("mtx", "-f", changer.Device, "load", fmt.Sprintf("%d", slotID), fmt.Sprintf("%d", driveID)) if err := mtxCmd.Run(); err != nil { // If mtx is not available, log and return error log.Printf("mtx command failed (may not be installed): %v", err) return fmt.Errorf("failed to load tape: mtx command failed (is mtx installed?)") } log.Printf("Loaded tape %s into drive %d", barcode, driveID) return nil } // EjectTape ejects a tape from a drive func (s *VTLService) EjectTape(driveID int) error { // Find the media changer device changers, err := s.ListMediaChangers() if err != nil || len(changers) == 0 { return fmt.Errorf("no media changer found") } changer := changers[0] // Use mtx command to unload tape: mtx -f /dev/sg0 unload mtxCmd := exec.Command("mtx", "-f", changer.Device, "unload", "1", fmt.Sprintf("%d", driveID)) if err := mtxCmd.Run(); err != nil { // If mtx is not available, log and return error log.Printf("mtx command failed (may not be installed): %v", err) return fmt.Errorf("failed to eject tape: mtx command failed (is mtx installed?)") } log.Printf("Ejected tape from drive %d", driveID) return nil } // GetMediaChangerStatus returns detailed status of the media changer func (s *VTLService) GetMediaChangerStatus() (*models.VTLMediaChanger, error) { changers, err := s.ListMediaChangers() if err != nil { return nil, err } if len(changers) == 0 { return nil, fmt.Errorf("no media changer found") } return &changers[0], nil } // parseDeviceConfig parses /etc/mhvtl/device.conf func (s *VTLService) parseDeviceConfig() (*DeviceConfig, error) { config := &DeviceConfig{ Libraries: []LibraryConfig{}, Drives: []DriveConfig{}, } file, err := os.Open(s.deviceConfigPath) if err != nil { return nil, err } defer file.Close() scanner := bufio.NewScanner(file) var currentLibraryID int var currentDrive *DriveConfig for scanner.Scan() { line := strings.TrimSpace(scanner.Text()) if line == "" || strings.HasPrefix(line, "#") { continue } // Parse Library line: "Library: 10 CHANNEL: 00 TARGET: 00 LUN: 00" if strings.HasPrefix(line, "Library:") { parts := strings.Fields(line) if len(parts) >= 2 { libID, _ := strconv.Atoi(parts[1]) currentLibraryID = libID // Create new library entry config.Libraries = append(config.Libraries, LibraryConfig{ ID: libID, }) } continue } // Parse Library attributes (only if we're not parsing a drive) if currentDrive == nil && currentLibraryID > 0 { if strings.HasPrefix(line, "Vendor identification:") { if len(config.Libraries) > 0 { idx := len(config.Libraries) - 1 config.Libraries[idx].Vendor = strings.TrimSpace(strings.TrimPrefix(line, "Vendor identification:")) } continue } if strings.HasPrefix(line, "Product identification:") { if len(config.Libraries) > 0 { idx := len(config.Libraries) - 1 config.Libraries[idx].Product = strings.TrimSpace(strings.TrimPrefix(line, "Product identification:")) } continue } if strings.HasPrefix(line, "Unit serial number:") { if len(config.Libraries) > 0 { idx := len(config.Libraries) - 1 config.Libraries[idx].Serial = strings.TrimSpace(strings.TrimPrefix(line, "Unit serial number:")) } continue } } // Parse Drive line: "Drive: 11 CHANNEL: 00 TARGET: 01 LUN: 00" if strings.HasPrefix(line, "Drive:") { parts := strings.Fields(line) if len(parts) >= 2 { driveID, _ := strconv.Atoi(parts[1]) currentDrive = &DriveConfig{ ID: driveID, LibraryID: currentLibraryID, } config.Drives = append(config.Drives, *currentDrive) } continue } // Reset currentDrive when we encounter a new Library or other section if strings.HasPrefix(line, "Library:") { currentDrive = nil } // Parse Drive attributes if currentDrive != nil && len(config.Drives) > 0 { idx := len(config.Drives) - 1 if strings.HasPrefix(line, "Library ID:") { parts := strings.Fields(line) if len(parts) >= 3 { libID, _ := strconv.Atoi(parts[2]) config.Drives[idx].LibraryID = libID } } if strings.HasPrefix(line, "Slot:") { parts := strings.Fields(line) if len(parts) >= 2 { slotID, _ := strconv.Atoi(parts[1]) config.Drives[idx].SlotID = slotID } } if strings.HasPrefix(line, "Vendor identification:") { config.Drives[idx].Vendor = strings.TrimSpace(strings.TrimPrefix(line, "Vendor identification:")) } if strings.HasPrefix(line, "Product identification:") { config.Drives[idx].Product = strings.TrimSpace(strings.TrimPrefix(line, "Product identification:")) } if strings.HasPrefix(line, "Unit serial number:") { config.Drives[idx].Serial = strings.TrimSpace(strings.TrimPrefix(line, "Unit serial number:")) } } } return config, scanner.Err() } // parseLibraryContents parses library_contents.X file func (s *VTLService) parseLibraryContents(libraryID int) (map[int]string, error) { // Map slot ID to barcode slotMap := make(map[int]string) contentsPath := fmt.Sprintf("/etc/mhvtl/library_contents.%d", libraryID) file, err := os.Open(contentsPath) if err != nil { return slotMap, err } defer file.Close() scanner := bufio.NewScanner(file) // Match "Slot 1: E01001L8" or "Slot 10: E01010L8" or "Slot 1: -" or "Slot 1:" format slotRegex := regexp.MustCompile(`^Slot\s+(\d+):\s*(.*)$`) for scanner.Scan() { line := strings.TrimSpace(scanner.Text()) if line == "" || strings.HasPrefix(line, "#") { continue } matches := slotRegex.FindStringSubmatch(line) if len(matches) >= 2 { slotID, err := strconv.Atoi(matches[1]) if err != nil { continue } // matches[2] is the barcode (or empty, or "-") barcode := strings.TrimSpace(matches[2]) // Only add to map if barcode exists and is not empty or "-" if barcode != "" && barcode != "-" { slotMap[slotID] = barcode } } } return slotMap, scanner.Err() } // findDeviceForDrive finds device path for a drive ID func (s *VTLService) findDeviceForDrive(driveID int) string { // Try to find device in /sys/class/scsi_tape/ tapePath := "/sys/class/scsi_tape" entries, err := os.ReadDir(tapePath) if err != nil { return fmt.Sprintf("/dev/st%d", driveID%10) // Fallback } for _, entry := range entries { if !entry.IsDir() { continue } deviceName := entry.Name() if !strings.HasPrefix(deviceName, "st") && !strings.HasPrefix(deviceName, "nst") { continue } // Check if this device matches the drive ID // This is a simplified check - in real implementation, we'd need to map device to drive ID deviceID := s.getDriveIDFromDevice(deviceName) if deviceID == driveID { return fmt.Sprintf("/dev/%s", deviceName) } } return fmt.Sprintf("/dev/st%d", driveID%10) // Fallback } // checkMediaLoadedByDriveID checks if media is loaded in a drive by drive ID func (s *VTLService) checkMediaLoadedByDriveID(driveID int) (bool, string) { devicePath := s.findDeviceForDrive(driveID) deviceName := filepath.Base(devicePath) return s.checkMediaLoaded(deviceName) } // findMediaChangerDevice finds device path for a media changer/library func (s *VTLService) findMediaChangerDevice(libraryID int) string { // Try to find in /sys/class/scsi_generic/ sgPath := "/sys/class/scsi_generic" entries, err := os.ReadDir(sgPath) if err != nil { return fmt.Sprintf("/dev/sg%d", libraryID%10) // Fallback } // Map library ID to device based on target in device.conf // Library 10 -> TARGET: 00 -> usually sg0 // Library 30 -> TARGET: 08 -> usually sg1 or sg2 deviceIndex := 0 if libraryID == 30 { deviceIndex = 1 } // Get all sg devices sorted sgDevices := []string{} for _, entry := range entries { if !entry.IsDir() { continue } deviceName := entry.Name() if strings.HasPrefix(deviceName, "sg") { sgDevices = append(sgDevices, deviceName) } } // Try to find matching device by checking vendor/product for _, deviceName := range sgDevices { devicePath := fmt.Sprintf("/dev/%s", deviceName) vendorPath := fmt.Sprintf("%s/%s/device/vendor", sgPath, deviceName) productPath := fmt.Sprintf("%s/%s/device/model", sgPath, deviceName) vendor := "" product := "" if vendorBytes, err := os.ReadFile(vendorPath); err == nil { vendor = strings.TrimSpace(string(vendorBytes)) } if productBytes, err := os.ReadFile(productPath); err == nil { product = strings.TrimSpace(string(productBytes)) } // Check if it's a media changer (STK L700 or L80) if strings.Contains(strings.ToUpper(vendor), "STK") || strings.Contains(strings.ToUpper(product), "L700") || strings.Contains(strings.ToUpper(product), "L80") { // For library 10, prefer L700; for library 30, prefer L80 if libraryID == 10 && strings.Contains(strings.ToUpper(product), "L700") { return devicePath } if libraryID == 30 && strings.Contains(strings.ToUpper(product), "L80") { return devicePath } // If we haven't found a specific match yet, use this one if deviceIndex < len(sgDevices) && deviceName == sgDevices[deviceIndex] { return devicePath } } } // Fallback: use device index if deviceIndex < len(sgDevices) { return fmt.Sprintf("/dev/%s", sgDevices[deviceIndex]) } return fmt.Sprintf("/dev/sg%d", deviceIndex) } // validateLTOBarcode validates that barcode follows LTO standard format // LTO barcodes typically end with L1-L9, LW, LT, LU, LV func (s *VTLService) validateLTOBarcode(barcode string) error { barcode = strings.ToUpper(strings.TrimSpace(barcode)) if barcode == "" { return fmt.Errorf("barcode cannot be empty") } // Check minimum length (at least 3 characters, e.g., "E01L5") if len(barcode) < 3 { return fmt.Errorf("barcode too short (minimum 3 characters)") } // Check suffix (last 2 characters must be valid LTO suffix) if len(barcode) < 2 { return fmt.Errorf("barcode must end with valid LTO suffix (L1-L9, LW, LT, LU, LV)") } suffix := barcode[len(barcode)-2:] validSuffixes := []string{"L1", "L2", "L3", "L4", "L5", "L6", "L7", "L8", "L9", "LW", "LT", "LU", "LV"} valid := false for _, vs := range validSuffixes { if suffix == vs { valid = true break } } if !valid { return fmt.Errorf("barcode must end with valid LTO suffix (L1-L9, LW, LT, LU, LV), got: %s", suffix) } return nil } // determineTapeTypeFromBarcode determines tape type from barcode suffix func (s *VTLService) determineTapeTypeFromBarcode(barcode string) string { barcode = strings.ToUpper(barcode) // Check suffix (last 2 characters) if len(barcode) >= 2 { suffix := barcode[len(barcode)-2:] switch suffix { case "L1": return "LTO-1" case "L2": return "LTO-2" case "L3": return "LTO-3" case "L4": return "LTO-4" case "L5": return "LTO-5" case "L6": return "LTO-6" case "L7": return "LTO-7" case "L8": return "LTO-8" case "L9": return "LTO-9" case "LW": return "LTO-6 WORM" case "LT": return "LTO-3 WORM" case "LU": return "LTO-4 WORM" case "LV": return "LTO-5 WORM" } } return "LTO-5" // Default } // getDefaultTapeSize returns default size in bytes for LTO generation func (s *VTLService) getDefaultTapeSize(tapeType string) uint64 { tapeType = strings.ToUpper(tapeType) // LTO capacities (compressed, in bytes) // LTO-1: 200 GB = 200 * 1024^3 bytes // LTO-2: 400 GB = 400 * 1024^3 bytes // LTO-3: 800 GB = 800 * 1024^3 bytes // LTO-4: 1.6 TB = 1600 * 1024^3 bytes // LTO-5: 3.0 TB = 3000 * 1024^3 bytes // LTO-6: 6.25 TB = 6250 * 1024^3 bytes // LTO-7: 15 TB = 15000 * 1024^3 bytes // LTO-8: 30 TB = 30000 * 1024^3 bytes // LTO-9: 45 TB = 45000 * 1024^3 bytes switch { case strings.Contains(tapeType, "LTO-9"): return 45 * 1024 * 1024 * 1024 * 1024 // 45 TB case strings.Contains(tapeType, "LTO-8"): return 30 * 1024 * 1024 * 1024 * 1024 // 30 TB case strings.Contains(tapeType, "LTO-7"): return 15 * 1024 * 1024 * 1024 * 1024 // 15 TB case strings.Contains(tapeType, "LTO-6"): return 6250 * 1024 * 1024 * 1024 // 6.25 TB case strings.Contains(tapeType, "LTO-5"): return 3000 * 1024 * 1024 * 1024 // 3 TB case strings.Contains(tapeType, "LTO-4"): return 1600 * 1024 * 1024 * 1024 // 1.6 TB case strings.Contains(tapeType, "LTO-3"): return 800 * 1024 * 1024 * 1024 // 800 GB case strings.Contains(tapeType, "LTO-2"): return 400 * 1024 * 1024 * 1024 // 400 GB case strings.Contains(tapeType, "LTO-1"): return 200 * 1024 * 1024 * 1024 // 200 GB default: return 3000 * 1024 * 1024 * 1024 // Default: 3 TB (LTO-5) } } // findTapeInLibrary finds which library and slot a tape is in func (s *VTLService) findTapeInLibrary(barcode string) (libraryID int, slotID int) { // Check all known libraries libraryIDs := []int{10, 30} // Also try to get from device.conf deviceConfig, err := s.parseDeviceConfig() if err == nil && len(deviceConfig.Libraries) > 0 { libraryIDs = []int{} for _, lib := range deviceConfig.Libraries { libraryIDs = append(libraryIDs, lib.ID) } } for _, libID := range libraryIDs { slotMap, err := s.parseLibraryContents(libID) if err == nil { for sid, bc := range slotMap { if bc == barcode { return libID, sid } } } } return 0, 0 } // removeTapeFromLibraryContents removes a tape from library_contents file func (s *VTLService) removeTapeFromLibraryContents(libraryID int, slotID int) error { contentsPath := fmt.Sprintf("/etc/mhvtl/library_contents.%d", libraryID) // Read the file file, err := os.OpenFile(contentsPath, os.O_RDWR, 0644) if err != nil { return fmt.Errorf("failed to open library_contents file: %v", err) } defer file.Close() // Read all lines var lines []string scanner := bufio.NewScanner(file) for scanner.Scan() { lines = append(lines, scanner.Text()) } if err := scanner.Err(); err != nil { return fmt.Errorf("failed to read library_contents file: %v", err) } // Find and clear the slot line (set to empty, don't remove the line) slotPattern := fmt.Sprintf("Slot %d:", slotID) updated := false var newLines []string for _, line := range lines { trimmed := strings.TrimSpace(line) if strings.HasPrefix(trimmed, slotPattern) { // Clear the slot by setting it to empty (format: "Slot X:" without "-") // Preserve original indentation indent := "" for i, char := range line { if char != ' ' && char != '\t' { indent = line[:i] break } } newLines = append(newLines, fmt.Sprintf("%sSlot %d:", indent, slotID)) updated = true } else { newLines = append(newLines, line) } } if !updated { log.Printf("Slot %d not found in library_contents.%d", slotID, libraryID) return nil // Not an error if slot not found } // Write back to file if err := file.Truncate(0); err != nil { return fmt.Errorf("failed to truncate file: %v", err) } if _, err := file.Seek(0, 0); err != nil { return fmt.Errorf("failed to seek file: %v", err) } writer := bufio.NewWriter(file) for _, line := range newLines { if _, err := writer.WriteString(line + "\n"); err != nil { return fmt.Errorf("failed to write line: %v", err) } } if err := writer.Flush(); err != nil { return fmt.Errorf("failed to flush file: %v", err) } log.Printf("Removed slot %d from library_contents.%d", slotID, libraryID) return nil } // addTapeToLibraryContents adds a tape to library_contents file func (s *VTLService) addTapeToLibraryContents(libraryID int, slotID int, barcode string) error { contentsPath := fmt.Sprintf("/etc/mhvtl/library_contents.%d", libraryID) // Read the file file, err := os.OpenFile(contentsPath, os.O_RDWR, 0644) if err != nil { return fmt.Errorf("failed to open library_contents file: %v", err) } defer file.Close() // Read all lines var lines []string scanner := bufio.NewScanner(file) for scanner.Scan() { lines = append(lines, scanner.Text()) } if err := scanner.Err(); err != nil { return fmt.Errorf("failed to read library_contents file: %v", err) } // Find and update the slot line slotPattern := fmt.Sprintf("Slot %d:", slotID) updated := false var newLines []string slotRegex := regexp.MustCompile(`^Slot\s+(\d+):\s*(.+)$`) for _, line := range lines { trimmed := strings.TrimSpace(line) if strings.HasPrefix(trimmed, slotPattern) { // Update the slot with barcode // Preserve original indentation indent := "" for i, char := range line { if char != ' ' && char != '\t' { indent = line[:i] break } } newLines = append(newLines, fmt.Sprintf("%sSlot %d: %s", indent, slotID, barcode)) updated = true } else { newLines = append(newLines, line) } } // If slot not found, add it at the end if !updated { // Find the highest slot number to determine where to add maxSlot := 0 for _, line := range lines { trimmed := strings.TrimSpace(line) matches := slotRegex.FindStringSubmatch(trimmed) if len(matches) == 3 { if sid, err := strconv.Atoi(matches[1]); err == nil && sid > maxSlot { maxSlot = sid } } } // Add new slot line newLines = append(newLines, fmt.Sprintf("Slot %d: %s", slotID, barcode)) updated = true } if !updated { return fmt.Errorf("failed to update slot %d in library_contents.%d", slotID, libraryID) } // Write back to file if err := file.Truncate(0); err != nil { return fmt.Errorf("failed to truncate file: %v", err) } if _, err := file.Seek(0, 0); err != nil { return fmt.Errorf("failed to seek file: %v", err) } writer := bufio.NewWriter(file) for _, line := range newLines { if _, err := writer.WriteString(line + "\n"); err != nil { return fmt.Errorf("failed to write line: %v", err) } } if err := writer.Flush(); err != nil { return fmt.Errorf("failed to flush file: %v", err) } log.Printf("Added tape %s to slot %d in library_contents.%d", barcode, slotID, libraryID) return nil } // ensureWritableFilesystem checks if filesystem is writable and remounts if needed func (s *VTLService) ensureWritableFilesystem(path string) error { // Try to create a test file to check if writable testFile := filepath.Join(path, ".write-test-check") if file, err := os.Create(testFile); err == nil { file.Close() os.Remove(testFile) return nil // Already writable } // Filesystem might be read-only, try to remount log.Printf("Filesystem appears read-only, attempting remount for %s", path) return s.remountReadWrite(path) } // remountReadWrite attempts to remount the filesystem containing the path as read-write func (s *VTLService) remountReadWrite(path string) error { // Find the mount point for the path // Try /opt first, then /, then use findmnt var mountPoint string // Check if path is under /opt if strings.HasPrefix(path, "/opt") { mountPoint = "/opt" } else { mountPoint = "/" } // Try to remount with sudo log.Printf("Attempting to remount %s as read-write", mountPoint) cmd := exec.Command("sudo", "mount", "-o", "remount,rw", mountPoint) if output, err := cmd.CombinedOutput(); err != nil { outputStr := strings.TrimSpace(string(output)) log.Printf("Remount failed for %s: %v, output: %s", mountPoint, err, outputStr) // Try alternative: remount root if mountPoint != "/" { log.Printf("Trying to remount root filesystem") cmd = exec.Command("sudo", "mount", "-o", "remount,rw", "/") if output, err := cmd.CombinedOutput(); err != nil { outputStr = strings.TrimSpace(string(output)) return fmt.Errorf("failed to remount filesystem: %v, output: %s", err, outputStr) } log.Printf("Root filesystem remounted as read-write") return nil } return fmt.Errorf("failed to remount %s: %v, output: %s", mountPoint, err, outputStr) } log.Printf("Filesystem %s remounted as read-write", mountPoint) return nil }