2295 lines
70 KiB
Go
2295 lines
70 KiB
Go
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 {
|
|
log.Printf("Warning: failed to parse device.conf: %v", err)
|
|
}
|
|
|
|
if err == nil && len(deviceConfig.Libraries) > 0 {
|
|
log.Printf("Found %d libraries in device.conf", len(deviceConfig.Libraries))
|
|
// 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 {
|
|
// Count all slots (including empty ones) by reading file directly
|
|
contentsPath := fmt.Sprintf("/etc/mhvtl/library_contents.%d", lib.ID)
|
|
if file, err := os.Open(contentsPath); err == nil {
|
|
scanner := bufio.NewScanner(file)
|
|
slotRegex := regexp.MustCompile(`^Slot\s+(\d+):`)
|
|
maxSlot := 0
|
|
for scanner.Scan() {
|
|
line := strings.TrimSpace(scanner.Text())
|
|
if line == "" || strings.HasPrefix(line, "#") {
|
|
continue
|
|
}
|
|
if matches := slotRegex.FindStringSubmatch(line); len(matches) >= 2 {
|
|
if slotID, err := strconv.Atoi(matches[1]); err == nil && slotID > maxSlot {
|
|
maxSlot = slotID
|
|
}
|
|
}
|
|
}
|
|
file.Close()
|
|
if maxSlot > 0 {
|
|
slotsPerLibrary[lib.ID] = maxSlot
|
|
} else {
|
|
// Fallback: try parseLibraryContents
|
|
slotMap, _ := s.parseLibraryContents(lib.ID)
|
|
slotsPerLibrary[lib.ID] = len(slotMap)
|
|
if slotsPerLibrary[lib.ID] == 0 {
|
|
slotsPerLibrary[lib.ID] = 10 // Default
|
|
}
|
|
}
|
|
} else {
|
|
// File doesn't exist, use default
|
|
slotsPerLibrary[lib.ID] = 10
|
|
}
|
|
}
|
|
|
|
// 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 <slot> <drive>
|
|
// 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 <slot> <drive>
|
|
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()
|
|
}
|
|
|
|
// writeDeviceConfig writes device.conf from DeviceConfig struct
|
|
func (s *VTLService) writeDeviceConfig(config *DeviceConfig) error {
|
|
// Ensure filesystem is writable (remount if needed)
|
|
parentDir := filepath.Dir(s.deviceConfigPath)
|
|
if err := s.ensureWritableFilesystem(parentDir); err != nil {
|
|
log.Printf("Warning: failed to ensure writable filesystem for %s: %v", parentDir, err)
|
|
// Continue anyway, might still work
|
|
}
|
|
|
|
// Create backup of existing config
|
|
backupPath := s.deviceConfigPath + ".backup"
|
|
if _, err := os.Stat(s.deviceConfigPath); err == nil {
|
|
// File exists, create backup
|
|
if err := exec.Command("cp", s.deviceConfigPath, backupPath).Run(); err != nil {
|
|
log.Printf("Warning: failed to create backup of device.conf: %v", err)
|
|
}
|
|
}
|
|
|
|
// Try to create file, if it fails due to read-only, try remount again
|
|
file, err := os.Create(s.deviceConfigPath)
|
|
if err != nil {
|
|
// Check if it's a read-only filesystem error
|
|
if strings.Contains(err.Error(), "read-only") || strings.Contains(err.Error(), "read only") {
|
|
log.Printf("Filesystem is read-only, attempting remount for %s", parentDir)
|
|
if remountErr := s.remountReadWrite(parentDir); remountErr != nil {
|
|
return fmt.Errorf("failed to remount filesystem as read-write: %v", remountErr)
|
|
}
|
|
// Try again after remount
|
|
file, err = os.Create(s.deviceConfigPath)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create device.conf after remount: %v", err)
|
|
}
|
|
} else {
|
|
return fmt.Errorf("failed to create device.conf: %v", err)
|
|
}
|
|
}
|
|
defer file.Close()
|
|
|
|
writer := bufio.NewWriter(file)
|
|
|
|
// Write libraries and their drives
|
|
for _, lib := range config.Libraries {
|
|
// Write library header
|
|
writer.WriteString(fmt.Sprintf("Library: %d CHANNEL: 00 TARGET: 00 LUN: 00\n", lib.ID))
|
|
if lib.Vendor != "" {
|
|
writer.WriteString(fmt.Sprintf("Vendor identification: %s\n", lib.Vendor))
|
|
} else {
|
|
writer.WriteString("Vendor identification: STK\n")
|
|
}
|
|
if lib.Product != "" {
|
|
writer.WriteString(fmt.Sprintf("Product identification: %s\n", lib.Product))
|
|
} else {
|
|
// Default product based on library ID
|
|
if lib.ID == 30 {
|
|
writer.WriteString("Product identification: L80\n")
|
|
} else {
|
|
writer.WriteString("Product identification: L700\n")
|
|
}
|
|
}
|
|
if lib.Serial != "" {
|
|
writer.WriteString(fmt.Sprintf("Unit serial number: %s\n", lib.Serial))
|
|
} else {
|
|
writer.WriteString(fmt.Sprintf("Unit serial number: %08d\n", lib.ID))
|
|
}
|
|
writer.WriteString("\n")
|
|
|
|
// Write drives for this library
|
|
for _, drive := range config.Drives {
|
|
if drive.LibraryID == lib.ID {
|
|
// Calculate CHANNEL, TARGET, LUN from drive ID
|
|
// Drive ID format: libraryID * 10 + slot (e.g., 11 = library 10, slot 1)
|
|
channel := "00"
|
|
target := fmt.Sprintf("%02d", (drive.ID%100)/10)
|
|
lun := "00"
|
|
|
|
writer.WriteString(fmt.Sprintf("Drive: %d CHANNEL: %s TARGET: %s LUN: %s\n", drive.ID, channel, target, lun))
|
|
writer.WriteString(fmt.Sprintf("Library ID: %d\n", drive.LibraryID))
|
|
writer.WriteString(fmt.Sprintf("Slot: %d\n", drive.SlotID))
|
|
if drive.Vendor != "" {
|
|
writer.WriteString(fmt.Sprintf("Vendor identification: %s\n", drive.Vendor))
|
|
} else {
|
|
writer.WriteString("Vendor identification: IBM\n")
|
|
}
|
|
if drive.Product != "" {
|
|
writer.WriteString(fmt.Sprintf("Product identification: %s\n", drive.Product))
|
|
} else {
|
|
writer.WriteString("Product identification: ULT3580-TD5\n")
|
|
}
|
|
if drive.Serial != "" {
|
|
writer.WriteString(fmt.Sprintf("Unit serial number: %s\n", drive.Serial))
|
|
} else {
|
|
writer.WriteString(fmt.Sprintf("Unit serial number: %08d\n", drive.ID))
|
|
}
|
|
writer.WriteString("\n")
|
|
}
|
|
}
|
|
}
|
|
|
|
// Flush and sync to ensure data is written to disk
|
|
if err := writer.Flush(); err != nil {
|
|
return fmt.Errorf("failed to flush device.conf: %v", err)
|
|
}
|
|
if err := file.Sync(); err != nil {
|
|
log.Printf("Warning: failed to sync device.conf: %v", err)
|
|
}
|
|
|
|
log.Printf("Successfully wrote device.conf with %d libraries and %d drives", len(config.Libraries), len(config.Drives))
|
|
return nil
|
|
}
|
|
|
|
// AddMediaChanger adds a new media changer/library to device.conf
|
|
func (s *VTLService) AddMediaChanger(libraryID int, vendor, product, serial string, numSlots int) error {
|
|
if libraryID <= 0 {
|
|
return fmt.Errorf("library ID must be greater than 0")
|
|
}
|
|
|
|
// Parse existing config
|
|
config, err := s.parseDeviceConfig()
|
|
if err != nil {
|
|
// If file doesn't exist, create new config
|
|
config = &DeviceConfig{
|
|
Libraries: []LibraryConfig{},
|
|
Drives: []DriveConfig{},
|
|
}
|
|
}
|
|
|
|
// Check if library already exists
|
|
for _, lib := range config.Libraries {
|
|
if lib.ID == libraryID {
|
|
return fmt.Errorf("library %d already exists", libraryID)
|
|
}
|
|
}
|
|
|
|
// Set defaults
|
|
if vendor == "" {
|
|
vendor = "STK"
|
|
}
|
|
if product == "" {
|
|
if libraryID == 30 {
|
|
product = "L80"
|
|
} else {
|
|
product = "L700"
|
|
}
|
|
}
|
|
if serial == "" {
|
|
serial = fmt.Sprintf("%08d", libraryID)
|
|
}
|
|
|
|
// Add new library
|
|
newLib := LibraryConfig{
|
|
ID: libraryID,
|
|
Vendor: vendor,
|
|
Product: product,
|
|
Serial: serial,
|
|
}
|
|
config.Libraries = append(config.Libraries, newLib)
|
|
|
|
// Write updated config
|
|
if err := s.writeDeviceConfig(config); err != nil {
|
|
return fmt.Errorf("failed to write device.conf: %v", err)
|
|
}
|
|
|
|
// Create library_contents file if it doesn't exist
|
|
contentsPath := fmt.Sprintf("/etc/mhvtl/library_contents.%d", libraryID)
|
|
if _, err := os.Stat(contentsPath); os.IsNotExist(err) {
|
|
// Ensure filesystem is writable
|
|
parentDir := filepath.Dir(contentsPath)
|
|
if err := s.ensureWritableFilesystem(parentDir); err != nil {
|
|
log.Printf("Warning: failed to ensure writable filesystem for %s: %v", parentDir, err)
|
|
}
|
|
|
|
file, err := os.Create(contentsPath)
|
|
if err != nil {
|
|
// If read-only, try remount
|
|
if strings.Contains(err.Error(), "read-only") || strings.Contains(err.Error(), "read only") {
|
|
if remountErr := s.remountReadWrite(parentDir); remountErr != nil {
|
|
log.Printf("Warning: failed to remount for library_contents: %v", remountErr)
|
|
} else {
|
|
// Retry after remount
|
|
file, err = os.Create(contentsPath)
|
|
}
|
|
}
|
|
if err != nil {
|
|
log.Printf("Warning: failed to create library_contents.%d: %v", libraryID, err)
|
|
} else {
|
|
defer file.Close()
|
|
// Write library_contents in correct format
|
|
file.WriteString("VERSION: 2\n\n")
|
|
// Drives will be added when drives are assigned to this library
|
|
file.WriteString("Picker 1:\n\n")
|
|
// Initialize with empty slots (MAP entries)
|
|
for i := 1; i <= numSlots; i++ {
|
|
file.WriteString(fmt.Sprintf("MAP %d:\n", i))
|
|
}
|
|
log.Printf("Created library_contents.%d with %d slots", libraryID, numSlots)
|
|
}
|
|
} else {
|
|
defer file.Close()
|
|
// Write library_contents in correct format
|
|
file.WriteString("VERSION: 2\n\n")
|
|
// Drives will be added when drives are assigned to this library
|
|
file.WriteString("Picker 1:\n\n")
|
|
// Initialize with empty slots (MAP entries)
|
|
for i := 1; i <= numSlots; i++ {
|
|
file.WriteString(fmt.Sprintf("MAP %d:\n", i))
|
|
}
|
|
log.Printf("Created library_contents.%d with %d slots", libraryID, numSlots)
|
|
}
|
|
}
|
|
|
|
// Restart mhvtl service to reflect changes (new library needs to be detected)
|
|
if err := s.RestartService(); err != nil {
|
|
log.Printf("Warning: failed to restart mhvtl service after adding media changer: %v", err)
|
|
// Continue even if restart fails - library is added to config
|
|
}
|
|
|
|
log.Printf("Added media changer: Library %d (%s %s)", libraryID, vendor, product)
|
|
return nil
|
|
}
|
|
|
|
// RemoveMediaChanger removes a media changer/library from device.conf
|
|
func (s *VTLService) RemoveMediaChanger(libraryID int) error {
|
|
if libraryID <= 0 {
|
|
return fmt.Errorf("library ID must be greater than 0")
|
|
}
|
|
|
|
// Parse existing config
|
|
config, err := s.parseDeviceConfig()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to parse device.conf: %v", err)
|
|
}
|
|
|
|
// Find and remove library
|
|
found := false
|
|
newLibraries := []LibraryConfig{}
|
|
for _, lib := range config.Libraries {
|
|
if lib.ID != libraryID {
|
|
newLibraries = append(newLibraries, lib)
|
|
} else {
|
|
found = true
|
|
}
|
|
}
|
|
|
|
if !found {
|
|
return fmt.Errorf("library %d not found", libraryID)
|
|
}
|
|
|
|
// Remove all drives associated with this library
|
|
newDrives := []DriveConfig{}
|
|
for _, drive := range config.Drives {
|
|
if drive.LibraryID != libraryID {
|
|
newDrives = append(newDrives, drive)
|
|
}
|
|
}
|
|
|
|
config.Libraries = newLibraries
|
|
config.Drives = newDrives
|
|
|
|
// Write updated config
|
|
if err := s.writeDeviceConfig(config); err != nil {
|
|
return fmt.Errorf("failed to write device.conf: %v", err)
|
|
}
|
|
|
|
// Optionally remove library_contents file (but keep it for safety)
|
|
// contentsPath := fmt.Sprintf("/etc/mhvtl/library_contents.%d", libraryID)
|
|
// os.Remove(contentsPath)
|
|
|
|
// Restart mhvtl service to reflect changes (library removal)
|
|
if err := s.RestartService(); err != nil {
|
|
log.Printf("Warning: failed to restart mhvtl service after removing media changer: %v", err)
|
|
// Continue even if restart fails - library is removed from config
|
|
}
|
|
|
|
log.Printf("Removed media changer: Library %d", libraryID)
|
|
return nil
|
|
}
|
|
|
|
// UpdateMediaChanger updates a media changer/library configuration
|
|
func (s *VTLService) UpdateMediaChanger(libraryID int, vendor, product, serial string) error {
|
|
if libraryID <= 0 {
|
|
return fmt.Errorf("library ID must be greater than 0")
|
|
}
|
|
|
|
// Parse existing config
|
|
config, err := s.parseDeviceConfig()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to parse device.conf: %v", err)
|
|
}
|
|
|
|
// Find and update library
|
|
found := false
|
|
for i := range config.Libraries {
|
|
if config.Libraries[i].ID == libraryID {
|
|
if vendor != "" {
|
|
config.Libraries[i].Vendor = vendor
|
|
}
|
|
if product != "" {
|
|
config.Libraries[i].Product = product
|
|
}
|
|
if serial != "" {
|
|
config.Libraries[i].Serial = serial
|
|
}
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if !found {
|
|
return fmt.Errorf("library %d not found", libraryID)
|
|
}
|
|
|
|
// Write updated config
|
|
if err := s.writeDeviceConfig(config); err != nil {
|
|
return fmt.Errorf("failed to write device.conf: %v", err)
|
|
}
|
|
|
|
// Restart mhvtl service to reflect changes (library config update)
|
|
if err := s.RestartService(); err != nil {
|
|
log.Printf("Warning: failed to restart mhvtl service after updating media changer: %v", err)
|
|
// Continue even if restart fails - library is updated in config
|
|
}
|
|
|
|
log.Printf("Updated media changer: Library %d", libraryID)
|
|
return nil
|
|
}
|
|
|
|
// AddDrive adds a new drive to device.conf
|
|
func (s *VTLService) AddDrive(driveID, libraryID, slotID int, vendor, product, serial string) error {
|
|
if driveID <= 0 {
|
|
return fmt.Errorf("drive ID must be greater than 0")
|
|
}
|
|
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")
|
|
}
|
|
|
|
// Parse existing config
|
|
config, err := s.parseDeviceConfig()
|
|
if err != nil {
|
|
// If file doesn't exist, create new config
|
|
config = &DeviceConfig{
|
|
Libraries: []LibraryConfig{},
|
|
Drives: []DriveConfig{},
|
|
}
|
|
}
|
|
|
|
// Check if library exists
|
|
libraryExists := false
|
|
for _, lib := range config.Libraries {
|
|
if lib.ID == libraryID {
|
|
libraryExists = true
|
|
break
|
|
}
|
|
}
|
|
if !libraryExists {
|
|
return fmt.Errorf("library %d does not exist", libraryID)
|
|
}
|
|
|
|
// Check if drive already exists
|
|
for _, drive := range config.Drives {
|
|
if drive.ID == driveID {
|
|
return fmt.Errorf("drive %d already exists", driveID)
|
|
}
|
|
}
|
|
|
|
// Calculate TARGET for this drive (used to determine device path)
|
|
// TARGET = (driveID % 100) / 10
|
|
// This ensures each drive gets a unique TARGET
|
|
newTarget := (driveID % 100) / 10
|
|
|
|
// Check for TARGET conflict with existing drives
|
|
// Each TARGET maps to a unique device path (/dev/stX), so we need to ensure uniqueness
|
|
for _, drive := range config.Drives {
|
|
existingTarget := (drive.ID % 100) / 10
|
|
if existingTarget == newTarget {
|
|
// TARGET maps to device: TARGET 01 -> /dev/st0, TARGET 02 -> /dev/st1, etc.
|
|
deviceNum := newTarget - 1
|
|
return fmt.Errorf("drive ID %d would conflict with drive %d (both use TARGET %02d, device /dev/st%d). Please use a different drive ID", driveID, drive.ID, existingTarget, deviceNum)
|
|
}
|
|
}
|
|
|
|
// Set defaults
|
|
if vendor == "" {
|
|
vendor = "IBM"
|
|
}
|
|
if product == "" {
|
|
product = "ULT3580-TD5"
|
|
}
|
|
if serial == "" {
|
|
serial = fmt.Sprintf("%08d", driveID)
|
|
}
|
|
|
|
// Add new drive
|
|
newDrive := DriveConfig{
|
|
ID: driveID,
|
|
LibraryID: libraryID,
|
|
SlotID: slotID,
|
|
Vendor: vendor,
|
|
Product: product,
|
|
Serial: serial,
|
|
}
|
|
config.Drives = append(config.Drives, newDrive)
|
|
|
|
// Write updated config
|
|
if err := s.writeDeviceConfig(config); err != nil {
|
|
return fmt.Errorf("failed to write device.conf: %v", err)
|
|
}
|
|
|
|
// Update library_contents to add Drive entry if not exists
|
|
if err := s.updateLibraryContentsForDrive(libraryID, driveID); err != nil {
|
|
log.Printf("Warning: failed to update library_contents.%d for drive %d: %v", libraryID, driveID, err)
|
|
// Continue even if library_contents update fails
|
|
}
|
|
|
|
// Restart mhvtl service to reflect changes (new drive needs to be detected)
|
|
if err := s.RestartService(); err != nil {
|
|
log.Printf("Warning: failed to restart mhvtl service after adding drive: %v", err)
|
|
// Continue even if restart fails - drive is added to config
|
|
}
|
|
|
|
log.Printf("Added drive: Drive %d (Library %d, Slot %d)", driveID, libraryID, slotID)
|
|
return nil
|
|
}
|
|
|
|
// RemoveDrive removes a drive from device.conf
|
|
func (s *VTLService) RemoveDrive(driveID int) error {
|
|
if driveID <= 0 {
|
|
return fmt.Errorf("drive ID must be greater than 0")
|
|
}
|
|
|
|
// Parse existing config
|
|
config, err := s.parseDeviceConfig()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to parse device.conf: %v", err)
|
|
}
|
|
|
|
// Find and remove drive
|
|
found := false
|
|
newDrives := []DriveConfig{}
|
|
for _, drive := range config.Drives {
|
|
if drive.ID != driveID {
|
|
newDrives = append(newDrives, drive)
|
|
} else {
|
|
found = true
|
|
}
|
|
}
|
|
|
|
if !found {
|
|
return fmt.Errorf("drive %d not found", driveID)
|
|
}
|
|
|
|
config.Drives = newDrives
|
|
|
|
// Write updated config
|
|
if err := s.writeDeviceConfig(config); err != nil {
|
|
return fmt.Errorf("failed to write device.conf: %v", err)
|
|
}
|
|
|
|
// Restart mhvtl service to reflect changes (drive removal)
|
|
if err := s.RestartService(); err != nil {
|
|
log.Printf("Warning: failed to restart mhvtl service after removing drive: %v", err)
|
|
// Continue even if restart fails - drive is removed from config
|
|
}
|
|
|
|
log.Printf("Removed drive: Drive %d", driveID)
|
|
return nil
|
|
}
|
|
|
|
// UpdateDrive updates a drive configuration
|
|
func (s *VTLService) UpdateDrive(driveID, libraryID, slotID int, vendor, product, serial string) error {
|
|
if driveID <= 0 {
|
|
return fmt.Errorf("drive ID must be greater than 0")
|
|
}
|
|
|
|
// Parse existing config
|
|
config, err := s.parseDeviceConfig()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to parse device.conf: %v", err)
|
|
}
|
|
|
|
// Find and update drive
|
|
found := false
|
|
for i := range config.Drives {
|
|
if config.Drives[i].ID == driveID {
|
|
if libraryID > 0 {
|
|
// Check if library exists
|
|
libraryExists := false
|
|
for _, lib := range config.Libraries {
|
|
if lib.ID == libraryID {
|
|
libraryExists = true
|
|
break
|
|
}
|
|
}
|
|
if !libraryExists {
|
|
return fmt.Errorf("library %d does not exist", libraryID)
|
|
}
|
|
config.Drives[i].LibraryID = libraryID
|
|
}
|
|
if slotID > 0 {
|
|
config.Drives[i].SlotID = slotID
|
|
}
|
|
if vendor != "" {
|
|
config.Drives[i].Vendor = vendor
|
|
}
|
|
if product != "" {
|
|
config.Drives[i].Product = product
|
|
}
|
|
if serial != "" {
|
|
config.Drives[i].Serial = serial
|
|
}
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if !found {
|
|
return fmt.Errorf("drive %d not found", driveID)
|
|
}
|
|
|
|
// Write updated config
|
|
if err := s.writeDeviceConfig(config); err != nil {
|
|
return fmt.Errorf("failed to write device.conf: %v", err)
|
|
}
|
|
|
|
// Restart mhvtl service to reflect changes (drive config update)
|
|
if err := s.RestartService(); err != nil {
|
|
log.Printf("Warning: failed to restart mhvtl service after updating drive: %v", err)
|
|
// Continue even if restart fails - drive is updated in config
|
|
}
|
|
|
|
log.Printf("Updated drive: Drive %d", driveID)
|
|
return nil
|
|
}
|
|
|
|
// 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
|
|
// In mhvtl, device path is determined by the order drives appear in device.conf
|
|
// TARGET number in device.conf maps to device: TARGET 01 -> /dev/st0, TARGET 02 -> /dev/st1, etc.
|
|
func (s *VTLService) findDeviceForDrive(driveID int) string {
|
|
// Parse device.conf to get TARGET for this drive
|
|
deviceConfig, err := s.parseDeviceConfig()
|
|
if err == nil {
|
|
// Find the drive and calculate its TARGET
|
|
for _, drive := range deviceConfig.Drives {
|
|
if drive.ID == driveID {
|
|
// Calculate TARGET from drive ID (same as in writeDeviceConfig)
|
|
target := (driveID % 100) / 10
|
|
// In mhvtl: TARGET 01 -> /dev/st0, TARGET 02 -> /dev/st1, TARGET 03 -> /dev/st2
|
|
// Device numbering is 0-based, TARGET is effectively 1-based for the ones digit
|
|
// So: TARGET 01 (ones=1) -> st0, TARGET 02 (ones=2) -> st1, etc.
|
|
return fmt.Sprintf("/dev/st%d", target-1)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Fallback: calculate TARGET from drive ID
|
|
target := (driveID % 100) / 10
|
|
if target > 0 {
|
|
return fmt.Sprintf("/dev/st%d", target-1) // TARGET 01 -> st0, TARGET 02 -> st1
|
|
}
|
|
return fmt.Sprintf("/dev/st0") // Default 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
|
|
}
|
|
|
|
// updateLibraryContentsForDrive adds or updates Drive entry in library_contents file
|
|
func (s *VTLService) updateLibraryContentsForDrive(libraryID, driveID int) error {
|
|
contentsPath := fmt.Sprintf("/etc/mhvtl/library_contents.%d", libraryID)
|
|
|
|
// Ensure filesystem is writable
|
|
parentDir := filepath.Dir(contentsPath)
|
|
if err := s.ensureWritableFilesystem(parentDir); err != nil {
|
|
log.Printf("Warning: failed to ensure writable filesystem for %s: %v", parentDir, err)
|
|
}
|
|
|
|
// Check if file exists, if not create it with proper format
|
|
file, err := os.OpenFile(contentsPath, os.O_RDWR|os.O_CREATE, 0644)
|
|
if err != nil {
|
|
// If read-only, try remount
|
|
if strings.Contains(err.Error(), "read-only") || strings.Contains(err.Error(), "read only") {
|
|
if remountErr := s.remountReadWrite(parentDir); remountErr != nil {
|
|
return fmt.Errorf("failed to remount filesystem: %v", remountErr)
|
|
}
|
|
// Retry after remount
|
|
file, err = os.OpenFile(contentsPath, os.O_RDWR|os.O_CREATE, 0644)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to open library_contents file after remount: %v", err)
|
|
}
|
|
} else {
|
|
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)
|
|
}
|
|
|
|
// If file is empty or doesn't have VERSION, create new format
|
|
if len(lines) == 0 {
|
|
lines = []string{
|
|
"VERSION: 2",
|
|
"",
|
|
"Picker 1:",
|
|
"",
|
|
}
|
|
} else {
|
|
// Check if VERSION exists
|
|
hasVersion := false
|
|
for _, line := range lines {
|
|
if strings.Contains(strings.TrimSpace(line), "VERSION:") {
|
|
hasVersion = true
|
|
break
|
|
}
|
|
}
|
|
if !hasVersion {
|
|
// Prepend VERSION
|
|
lines = append([]string{"VERSION: 2", ""}, lines...)
|
|
}
|
|
}
|
|
|
|
// Find drive number (sequential number for drives in this library)
|
|
// Count existing drives
|
|
driveRegex := regexp.MustCompile(`^Drive\s+(\d+):`)
|
|
maxDriveNum := 0
|
|
driveExists := false
|
|
driveNum := 0
|
|
|
|
for _, line := range lines {
|
|
trimmed := strings.TrimSpace(line)
|
|
if matches := driveRegex.FindStringSubmatch(trimmed); len(matches) >= 2 {
|
|
if num, err := strconv.Atoi(matches[1]); err == nil {
|
|
if num > maxDriveNum {
|
|
maxDriveNum = num
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check if this drive already exists (by checking device.conf drive ID mapping)
|
|
// For now, we'll use sequential numbering
|
|
// Find if drive entry already exists by checking all drives in device.conf for this library
|
|
deviceConfig, err := s.parseDeviceConfig()
|
|
if err == nil {
|
|
driveIndex := 1
|
|
for _, drive := range deviceConfig.Drives {
|
|
if drive.LibraryID == libraryID {
|
|
if drive.ID == driveID {
|
|
driveNum = driveIndex
|
|
break
|
|
}
|
|
driveIndex++
|
|
}
|
|
}
|
|
if driveNum == 0 {
|
|
driveNum = maxDriveNum + 1
|
|
}
|
|
} else {
|
|
driveNum = maxDriveNum + 1
|
|
}
|
|
|
|
// Check if Drive entry already exists
|
|
drivePattern := fmt.Sprintf("Drive %d:", driveNum)
|
|
for _, line := range lines {
|
|
if strings.Contains(strings.TrimSpace(line), drivePattern) {
|
|
driveExists = true
|
|
break
|
|
}
|
|
}
|
|
|
|
// If drive already exists, no need to update
|
|
if driveExists {
|
|
return nil
|
|
}
|
|
|
|
// Build new lines
|
|
var newLines []string
|
|
insertedDrive := false
|
|
afterVersion := false
|
|
pickerFound := false
|
|
|
|
for _, line := range lines {
|
|
trimmed := strings.TrimSpace(line)
|
|
|
|
// Track position after VERSION
|
|
if strings.Contains(trimmed, "VERSION:") {
|
|
afterVersion = true
|
|
newLines = append(newLines, line)
|
|
continue
|
|
}
|
|
|
|
// Insert Drive entries after VERSION and before Picker
|
|
if afterVersion && !insertedDrive {
|
|
if strings.HasPrefix(trimmed, "Picker") {
|
|
pickerFound = true
|
|
// Insert drive before Picker
|
|
newLines = append(newLines, fmt.Sprintf("Drive %d:", driveNum))
|
|
newLines = append(newLines, "") // Empty line
|
|
insertedDrive = true
|
|
} else if strings.HasPrefix(trimmed, "MAP") && !pickerFound {
|
|
// If no Picker found, insert before MAP
|
|
newLines = append(newLines, fmt.Sprintf("Drive %d:", driveNum))
|
|
newLines = append(newLines, "") // Empty line
|
|
insertedDrive = true
|
|
}
|
|
}
|
|
|
|
newLines = append(newLines, line)
|
|
}
|
|
|
|
// If drive wasn't inserted, add it at appropriate position
|
|
if !insertedDrive {
|
|
// Find position to insert (after VERSION, before Picker or MAP)
|
|
insertPos := -1
|
|
for idx, line := range newLines {
|
|
trimmed := strings.TrimSpace(line)
|
|
if strings.HasPrefix(trimmed, "Picker") || strings.HasPrefix(trimmed, "MAP") {
|
|
insertPos = idx
|
|
break
|
|
}
|
|
}
|
|
if insertPos == -1 {
|
|
// Add at end
|
|
newLines = append(newLines, fmt.Sprintf("Drive %d:", driveNum))
|
|
} else {
|
|
// Insert at position
|
|
newLines = append(newLines[:insertPos], append([]string{fmt.Sprintf("Drive %d:", driveNum), ""}, newLines[insertPos:]...)...)
|
|
}
|
|
}
|
|
|
|
// Write back to file
|
|
if err := file.Truncate(0); err != nil {
|
|
return fmt.Errorf("failed to truncate library_contents file: %v", err)
|
|
}
|
|
if _, err := file.Seek(0, 0); err != nil {
|
|
return fmt.Errorf("failed to seek library_contents file: %v", err)
|
|
}
|
|
|
|
writer := bufio.NewWriter(file)
|
|
for _, line := range newLines {
|
|
writer.WriteString(line + "\n")
|
|
}
|
|
if err := writer.Flush(); err != nil {
|
|
return fmt.Errorf("failed to flush library_contents file: %v", err)
|
|
}
|
|
if err := file.Sync(); err != nil {
|
|
log.Printf("Warning: failed to sync library_contents file: %v", err)
|
|
}
|
|
|
|
log.Printf("Updated library_contents.%d with Drive %d entry", libraryID, driveNum)
|
|
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 or /etc
|
|
if strings.HasPrefix(path, "/opt") {
|
|
mountPoint = "/opt"
|
|
} else if strings.HasPrefix(path, "/etc") {
|
|
// For /etc, we need to remount root filesystem
|
|
mountPoint = "/"
|
|
} 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
|
|
}
|