580 lines
18 KiB
Go
580 lines
18 KiB
Go
package tape_vtl
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"database/sql"
|
|
|
|
"github.com/atlasos/calypso/internal/common/database"
|
|
"github.com/atlasos/calypso/internal/common/logger"
|
|
)
|
|
|
|
// MHVTLMonitor monitors mhvtl configuration files and syncs to database
|
|
type MHVTLMonitor struct {
|
|
service *Service
|
|
logger *logger.Logger
|
|
configPath string
|
|
interval time.Duration
|
|
stopCh chan struct{}
|
|
}
|
|
|
|
// NewMHVTLMonitor creates a new MHVTL monitor service
|
|
func NewMHVTLMonitor(db *database.DB, log *logger.Logger, configPath string, interval time.Duration) *MHVTLMonitor {
|
|
return &MHVTLMonitor{
|
|
service: NewService(db, log),
|
|
logger: log,
|
|
configPath: configPath,
|
|
interval: interval,
|
|
stopCh: make(chan struct{}),
|
|
}
|
|
}
|
|
|
|
// Start starts the MHVTL monitor background service
|
|
func (m *MHVTLMonitor) Start(ctx context.Context) {
|
|
m.logger.Info("Starting MHVTL monitor service", "config_path", m.configPath, "interval", m.interval)
|
|
ticker := time.NewTicker(m.interval)
|
|
defer ticker.Stop()
|
|
|
|
// Run initial sync immediately
|
|
m.syncMHVTL(ctx)
|
|
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
m.logger.Info("MHVTL monitor service stopped")
|
|
return
|
|
case <-m.stopCh:
|
|
m.logger.Info("MHVTL monitor service stopped")
|
|
return
|
|
case <-ticker.C:
|
|
m.syncMHVTL(ctx)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Stop stops the MHVTL monitor service
|
|
func (m *MHVTLMonitor) Stop() {
|
|
close(m.stopCh)
|
|
}
|
|
|
|
// syncMHVTL parses mhvtl configuration and syncs to database
|
|
func (m *MHVTLMonitor) syncMHVTL(ctx context.Context) {
|
|
m.logger.Info("Running MHVTL configuration sync")
|
|
|
|
deviceConfPath := filepath.Join(m.configPath, "device.conf")
|
|
if _, err := os.Stat(deviceConfPath); os.IsNotExist(err) {
|
|
m.logger.Warn("MHVTL device.conf not found", "path", deviceConfPath)
|
|
return
|
|
}
|
|
|
|
// Parse device.conf to get libraries and drives
|
|
libraries, drives, err := m.parseDeviceConf(ctx, deviceConfPath)
|
|
if err != nil {
|
|
m.logger.Error("Failed to parse device.conf", "error", err)
|
|
return
|
|
}
|
|
|
|
m.logger.Info("Parsed MHVTL configuration", "libraries", len(libraries), "drives", len(drives))
|
|
|
|
// Log parsed drives for debugging
|
|
for _, drive := range drives {
|
|
m.logger.Debug("Parsed drive", "drive_id", drive.DriveID, "library_id", drive.LibraryID, "slot", drive.Slot)
|
|
}
|
|
|
|
// Sync libraries to database
|
|
for _, lib := range libraries {
|
|
if err := m.syncLibrary(ctx, lib); err != nil {
|
|
m.logger.Error("Failed to sync library", "library_id", lib.LibraryID, "error", err)
|
|
}
|
|
}
|
|
|
|
// Sync drives to database
|
|
for _, drive := range drives {
|
|
if err := m.syncDrive(ctx, drive); err != nil {
|
|
m.logger.Error("Failed to sync drive", "drive_id", drive.DriveID, "library_id", drive.LibraryID, "slot", drive.Slot, "error", err)
|
|
} else {
|
|
m.logger.Debug("Synced drive", "drive_id", drive.DriveID, "library_id", drive.LibraryID, "slot", drive.Slot)
|
|
}
|
|
}
|
|
|
|
// Parse library_contents files to get tapes
|
|
for _, lib := range libraries {
|
|
contentsPath := filepath.Join(m.configPath, fmt.Sprintf("library_contents.%d", lib.LibraryID))
|
|
if err := m.syncLibraryContents(ctx, lib.LibraryID, contentsPath); err != nil {
|
|
m.logger.Warn("Failed to sync library contents", "library_id", lib.LibraryID, "error", err)
|
|
}
|
|
}
|
|
|
|
m.logger.Info("MHVTL configuration sync completed")
|
|
}
|
|
|
|
// LibraryInfo represents a library from device.conf
|
|
type LibraryInfo struct {
|
|
LibraryID int
|
|
Vendor string
|
|
Product string
|
|
SerialNumber string
|
|
HomeDirectory string
|
|
Channel string
|
|
Target string
|
|
LUN string
|
|
}
|
|
|
|
// DriveInfo represents a drive from device.conf
|
|
type DriveInfo struct {
|
|
DriveID int
|
|
LibraryID int
|
|
Slot int
|
|
Vendor string
|
|
Product string
|
|
SerialNumber string
|
|
Channel string
|
|
Target string
|
|
LUN string
|
|
}
|
|
|
|
// parseDeviceConf parses mhvtl device.conf file
|
|
func (m *MHVTLMonitor) parseDeviceConf(ctx context.Context, path string) ([]LibraryInfo, []DriveInfo, error) {
|
|
file, err := os.Open(path)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("failed to open device.conf: %w", err)
|
|
}
|
|
defer file.Close()
|
|
|
|
var libraries []LibraryInfo
|
|
var drives []DriveInfo
|
|
|
|
scanner := bufio.NewScanner(file)
|
|
var currentLibrary *LibraryInfo
|
|
var currentDrive *DriveInfo
|
|
|
|
libraryRegex := regexp.MustCompile(`^Library:\s+(\d+)\s+CHANNEL:\s+(\S+)\s+TARGET:\s+(\S+)\s+LUN:\s+(\S+)`)
|
|
driveRegex := regexp.MustCompile(`^Drive:\s+(\d+)\s+CHANNEL:\s+(\S+)\s+TARGET:\s+(\S+)\s+LUN:\s+(\S+)`)
|
|
libraryIDRegex := regexp.MustCompile(`Library ID:\s+(\d+)\s+Slot:\s+(\d+)`)
|
|
|
|
for scanner.Scan() {
|
|
line := strings.TrimSpace(scanner.Text())
|
|
|
|
// Skip comments and empty lines
|
|
if strings.HasPrefix(line, "#") || line == "" {
|
|
continue
|
|
}
|
|
|
|
// Check for Library entry
|
|
if matches := libraryRegex.FindStringSubmatch(line); matches != nil {
|
|
if currentLibrary != nil {
|
|
libraries = append(libraries, *currentLibrary)
|
|
}
|
|
libID, _ := strconv.Atoi(matches[1])
|
|
currentLibrary = &LibraryInfo{
|
|
LibraryID: libID,
|
|
Channel: matches[2],
|
|
Target: matches[3],
|
|
LUN: matches[4],
|
|
}
|
|
currentDrive = nil
|
|
continue
|
|
}
|
|
|
|
// Check for Drive entry
|
|
if matches := driveRegex.FindStringSubmatch(line); matches != nil {
|
|
if currentDrive != nil {
|
|
drives = append(drives, *currentDrive)
|
|
}
|
|
driveID, _ := strconv.Atoi(matches[1])
|
|
currentDrive = &DriveInfo{
|
|
DriveID: driveID,
|
|
Channel: matches[2],
|
|
Target: matches[3],
|
|
LUN: matches[4],
|
|
}
|
|
// Library ID and Slot might be on the same line or next line
|
|
if matches := libraryIDRegex.FindStringSubmatch(line); matches != nil {
|
|
libID, _ := strconv.Atoi(matches[1])
|
|
slot, _ := strconv.Atoi(matches[2])
|
|
currentDrive.LibraryID = libID
|
|
currentDrive.Slot = slot
|
|
}
|
|
continue
|
|
}
|
|
|
|
// Parse library fields (only if we're in a library section and not in a drive section)
|
|
if currentLibrary != nil && currentDrive == nil {
|
|
// Handle both "Vendor identification:" and " Vendor identification:" (with leading space)
|
|
if strings.Contains(line, "Vendor identification:") {
|
|
parts := strings.Split(line, "Vendor identification:")
|
|
if len(parts) > 1 {
|
|
currentLibrary.Vendor = strings.TrimSpace(parts[1])
|
|
m.logger.Debug("Parsed vendor", "vendor", currentLibrary.Vendor, "library_id", currentLibrary.LibraryID)
|
|
}
|
|
} else if strings.Contains(line, "Product identification:") {
|
|
parts := strings.Split(line, "Product identification:")
|
|
if len(parts) > 1 {
|
|
currentLibrary.Product = strings.TrimSpace(parts[1])
|
|
m.logger.Info("Parsed library product", "product", currentLibrary.Product, "library_id", currentLibrary.LibraryID)
|
|
}
|
|
} else if strings.Contains(line, "Unit serial number:") {
|
|
parts := strings.Split(line, "Unit serial number:")
|
|
if len(parts) > 1 {
|
|
currentLibrary.SerialNumber = strings.TrimSpace(parts[1])
|
|
}
|
|
} else if strings.Contains(line, "Home directory:") {
|
|
parts := strings.Split(line, "Home directory:")
|
|
if len(parts) > 1 {
|
|
currentLibrary.HomeDirectory = strings.TrimSpace(parts[1])
|
|
}
|
|
}
|
|
}
|
|
|
|
// Parse drive fields
|
|
if currentDrive != nil {
|
|
// Check for Library ID and Slot first (can be on separate line)
|
|
if strings.Contains(line, "Library ID:") && strings.Contains(line, "Slot:") {
|
|
matches := libraryIDRegex.FindStringSubmatch(line)
|
|
if matches != nil {
|
|
libID, _ := strconv.Atoi(matches[1])
|
|
slot, _ := strconv.Atoi(matches[2])
|
|
currentDrive.LibraryID = libID
|
|
currentDrive.Slot = slot
|
|
m.logger.Debug("Parsed drive Library ID and Slot", "drive_id", currentDrive.DriveID, "library_id", libID, "slot", slot)
|
|
continue
|
|
}
|
|
}
|
|
// Handle both "Vendor identification:" and " Vendor identification:" (with leading space)
|
|
if strings.Contains(line, "Vendor identification:") {
|
|
parts := strings.Split(line, "Vendor identification:")
|
|
if len(parts) > 1 {
|
|
currentDrive.Vendor = strings.TrimSpace(parts[1])
|
|
}
|
|
} else if strings.Contains(line, "Product identification:") {
|
|
parts := strings.Split(line, "Product identification:")
|
|
if len(parts) > 1 {
|
|
currentDrive.Product = strings.TrimSpace(parts[1])
|
|
}
|
|
} else if strings.Contains(line, "Unit serial number:") {
|
|
parts := strings.Split(line, "Unit serial number:")
|
|
if len(parts) > 1 {
|
|
currentDrive.SerialNumber = strings.TrimSpace(parts[1])
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Add last library and drive
|
|
if currentLibrary != nil {
|
|
libraries = append(libraries, *currentLibrary)
|
|
}
|
|
if currentDrive != nil {
|
|
drives = append(drives, *currentDrive)
|
|
}
|
|
|
|
if err := scanner.Err(); err != nil {
|
|
return nil, nil, fmt.Errorf("error reading device.conf: %w", err)
|
|
}
|
|
|
|
return libraries, drives, nil
|
|
}
|
|
|
|
// syncLibrary syncs a library to database
|
|
func (m *MHVTLMonitor) syncLibrary(ctx context.Context, libInfo LibraryInfo) error {
|
|
// Check if library exists by mhvtl_library_id
|
|
var existingID string
|
|
err := m.service.db.QueryRowContext(ctx,
|
|
"SELECT id FROM virtual_tape_libraries WHERE mhvtl_library_id = $1",
|
|
libInfo.LibraryID,
|
|
).Scan(&existingID)
|
|
|
|
m.logger.Debug("Syncing library", "library_id", libInfo.LibraryID, "vendor", libInfo.Vendor, "product", libInfo.Product)
|
|
|
|
// Use product identification for library name (without library ID)
|
|
libraryName := fmt.Sprintf("VTL-%d", libInfo.LibraryID)
|
|
if libInfo.Product != "" {
|
|
// Use only product name, without library ID
|
|
libraryName = libInfo.Product
|
|
m.logger.Info("Using product for library name", "product", libInfo.Product, "library_id", libInfo.LibraryID, "name", libraryName)
|
|
} else if libInfo.Vendor != "" {
|
|
libraryName = libInfo.Vendor
|
|
m.logger.Info("Using vendor for library name (product not available)", "vendor", libInfo.Vendor, "library_id", libInfo.LibraryID)
|
|
}
|
|
|
|
if err == sql.ErrNoRows {
|
|
// Create new library
|
|
// Get backing store path from mhvtl.conf
|
|
backingStorePath := "/opt/mhvtl"
|
|
if libInfo.HomeDirectory != "" {
|
|
backingStorePath = libInfo.HomeDirectory
|
|
}
|
|
|
|
// Count slots and drives from library_contents file
|
|
contentsPath := filepath.Join(m.configPath, fmt.Sprintf("library_contents.%d", libInfo.LibraryID))
|
|
slotCount, driveCount := m.countSlotsAndDrives(contentsPath)
|
|
|
|
_, err = m.service.db.ExecContext(ctx, `
|
|
INSERT INTO virtual_tape_libraries (
|
|
name, description, mhvtl_library_id, backing_store_path,
|
|
vendor, slot_count, drive_count, is_active
|
|
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
|
`, libraryName, fmt.Sprintf("MHVTL Library %d (%s)", libInfo.LibraryID, libInfo.Product),
|
|
libInfo.LibraryID, backingStorePath, libInfo.Vendor, slotCount, driveCount, true)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to insert library: %w", err)
|
|
}
|
|
m.logger.Info("Created virtual library from MHVTL", "library_id", libInfo.LibraryID, "name", libraryName)
|
|
} else if err == nil {
|
|
// Update existing library - also update name if product is available
|
|
updateName := libraryName
|
|
// If product exists and current name doesn't match, update it
|
|
if libInfo.Product != "" {
|
|
var currentName string
|
|
err := m.service.db.QueryRowContext(ctx,
|
|
"SELECT name FROM virtual_tape_libraries WHERE id = $1", existingID,
|
|
).Scan(¤tName)
|
|
if err == nil {
|
|
// Use only product name, without library ID
|
|
expectedName := libInfo.Product
|
|
if currentName != expectedName {
|
|
updateName = expectedName
|
|
m.logger.Info("Updating library name", "old", currentName, "new", updateName, "product", libInfo.Product)
|
|
}
|
|
}
|
|
}
|
|
|
|
m.logger.Info("Updating existing library", "library_id", libInfo.LibraryID, "product", libInfo.Product, "vendor", libInfo.Vendor, "old_name", libraryName, "new_name", updateName)
|
|
_, err = m.service.db.ExecContext(ctx, `
|
|
UPDATE virtual_tape_libraries SET
|
|
name = $1, description = $2, backing_store_path = $3,
|
|
vendor = $4, is_active = $5, updated_at = NOW()
|
|
WHERE id = $6
|
|
`, updateName, fmt.Sprintf("MHVTL Library %d (%s)", libInfo.LibraryID, libInfo.Product),
|
|
libInfo.HomeDirectory, libInfo.Vendor, true, existingID)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to update library: %w", err)
|
|
}
|
|
m.logger.Debug("Updated virtual library from MHVTL", "library_id", libInfo.LibraryID)
|
|
} else {
|
|
return fmt.Errorf("failed to check library existence: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// syncDrive syncs a drive to database
|
|
func (m *MHVTLMonitor) syncDrive(ctx context.Context, driveInfo DriveInfo) error {
|
|
// Get library ID from mhvtl_library_id
|
|
var libraryID string
|
|
err := m.service.db.QueryRowContext(ctx,
|
|
"SELECT id FROM virtual_tape_libraries WHERE mhvtl_library_id = $1",
|
|
driveInfo.LibraryID,
|
|
).Scan(&libraryID)
|
|
if err != nil {
|
|
return fmt.Errorf("library not found for drive: %w", err)
|
|
}
|
|
|
|
// Calculate drive number from slot (drives are typically in slots 1, 2, 3, etc.)
|
|
driveNumber := driveInfo.Slot
|
|
|
|
// Check if drive exists
|
|
var existingID string
|
|
err = m.service.db.QueryRowContext(ctx,
|
|
"SELECT id FROM virtual_tape_drives WHERE library_id = $1 AND drive_number = $2",
|
|
libraryID, driveNumber,
|
|
).Scan(&existingID)
|
|
|
|
// Get device path (typically /dev/stX or /dev/nstX)
|
|
devicePath := fmt.Sprintf("/dev/st%d", driveInfo.DriveID-10) // Drive 11 -> st1, Drive 12 -> st2, etc.
|
|
stablePath := fmt.Sprintf("/dev/tape/by-id/scsi-%s", driveInfo.SerialNumber)
|
|
|
|
if err == sql.ErrNoRows {
|
|
// Create new drive
|
|
_, err = m.service.db.ExecContext(ctx, `
|
|
INSERT INTO virtual_tape_drives (
|
|
library_id, drive_number, device_path, stable_path, status, is_active
|
|
) VALUES ($1, $2, $3, $4, $5, $6)
|
|
`, libraryID, driveNumber, devicePath, stablePath, "idle", true)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to insert drive: %w", err)
|
|
}
|
|
m.logger.Info("Created virtual drive from MHVTL", "drive_id", driveInfo.DriveID, "library_id", driveInfo.LibraryID)
|
|
} else if err == nil {
|
|
// Update existing drive
|
|
_, err = m.service.db.ExecContext(ctx, `
|
|
UPDATE virtual_tape_drives SET
|
|
device_path = $1, stable_path = $2, is_active = $3, updated_at = NOW()
|
|
WHERE id = $4
|
|
`, devicePath, stablePath, true, existingID)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to update drive: %w", err)
|
|
}
|
|
m.logger.Debug("Updated virtual drive from MHVTL", "drive_id", driveInfo.DriveID)
|
|
} else {
|
|
return fmt.Errorf("failed to check drive existence: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// syncLibraryContents syncs tapes from library_contents file
|
|
func (m *MHVTLMonitor) syncLibraryContents(ctx context.Context, libraryID int, contentsPath string) error {
|
|
// Get library ID from database
|
|
var dbLibraryID string
|
|
err := m.service.db.QueryRowContext(ctx,
|
|
"SELECT id FROM virtual_tape_libraries WHERE mhvtl_library_id = $1",
|
|
libraryID,
|
|
).Scan(&dbLibraryID)
|
|
if err != nil {
|
|
return fmt.Errorf("library not found: %w", err)
|
|
}
|
|
|
|
// Get backing store path
|
|
var backingStorePath string
|
|
err = m.service.db.QueryRowContext(ctx,
|
|
"SELECT backing_store_path FROM virtual_tape_libraries WHERE id = $1",
|
|
dbLibraryID,
|
|
).Scan(&backingStorePath)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get backing store path: %w", err)
|
|
}
|
|
|
|
file, err := os.Open(contentsPath)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to open library_contents file: %w", err)
|
|
}
|
|
defer file.Close()
|
|
|
|
scanner := bufio.NewScanner(file)
|
|
slotRegex := regexp.MustCompile(`^Slot\s+(\d+):\s+(.+)`)
|
|
|
|
for scanner.Scan() {
|
|
line := strings.TrimSpace(scanner.Text())
|
|
|
|
// Skip comments and empty lines
|
|
if strings.HasPrefix(line, "#") || line == "" {
|
|
continue
|
|
}
|
|
|
|
matches := slotRegex.FindStringSubmatch(line)
|
|
if matches != nil {
|
|
slotNumber, _ := strconv.Atoi(matches[1])
|
|
barcode := strings.TrimSpace(matches[2])
|
|
|
|
if barcode == "" || barcode == "?" {
|
|
continue // Empty slot
|
|
}
|
|
|
|
// Determine tape type from barcode suffix
|
|
tapeType := "LTO-8" // Default
|
|
if len(barcode) >= 2 {
|
|
suffix := barcode[len(barcode)-2:]
|
|
switch suffix {
|
|
case "L1":
|
|
tapeType = "LTO-1"
|
|
case "L2":
|
|
tapeType = "LTO-2"
|
|
case "L3":
|
|
tapeType = "LTO-3"
|
|
case "L4":
|
|
tapeType = "LTO-4"
|
|
case "L5":
|
|
tapeType = "LTO-5"
|
|
case "L6":
|
|
tapeType = "LTO-6"
|
|
case "L7":
|
|
tapeType = "LTO-7"
|
|
case "L8":
|
|
tapeType = "LTO-8"
|
|
case "L9":
|
|
tapeType = "LTO-9"
|
|
}
|
|
}
|
|
|
|
// Check if tape exists
|
|
var existingID string
|
|
err := m.service.db.QueryRowContext(ctx,
|
|
"SELECT id FROM virtual_tapes WHERE library_id = $1 AND barcode = $2",
|
|
dbLibraryID, barcode,
|
|
).Scan(&existingID)
|
|
|
|
imagePath := filepath.Join(backingStorePath, "tapes", fmt.Sprintf("%s.img", barcode))
|
|
defaultSize := int64(15 * 1024 * 1024 * 1024 * 1024) // 15 TB default for LTO-8
|
|
|
|
if err == sql.ErrNoRows {
|
|
// Create new tape
|
|
_, err = m.service.db.ExecContext(ctx, `
|
|
INSERT INTO virtual_tapes (
|
|
library_id, barcode, slot_number, image_file_path,
|
|
size_bytes, used_bytes, tape_type, status
|
|
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
|
`, dbLibraryID, barcode, slotNumber, imagePath, defaultSize, 0, tapeType, "idle")
|
|
if err != nil {
|
|
m.logger.Warn("Failed to insert tape", "barcode", barcode, "error", err)
|
|
} else {
|
|
m.logger.Debug("Created virtual tape from MHVTL", "barcode", barcode, "slot", slotNumber)
|
|
}
|
|
} else if err == nil {
|
|
// Update existing tape slot
|
|
_, err = m.service.db.ExecContext(ctx, `
|
|
UPDATE virtual_tapes SET
|
|
slot_number = $1, tape_type = $2, updated_at = NOW()
|
|
WHERE id = $3
|
|
`, slotNumber, tapeType, existingID)
|
|
if err != nil {
|
|
m.logger.Warn("Failed to update tape", "barcode", barcode, "error", err)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return scanner.Err()
|
|
}
|
|
|
|
// countSlotsAndDrives counts slots and drives from library_contents file
|
|
func (m *MHVTLMonitor) countSlotsAndDrives(contentsPath string) (slotCount, driveCount int) {
|
|
file, err := os.Open(contentsPath)
|
|
if err != nil {
|
|
return 10, 2 // Default values
|
|
}
|
|
defer file.Close()
|
|
|
|
scanner := bufio.NewScanner(file)
|
|
slotRegex := regexp.MustCompile(`^Slot\s+(\d+):`)
|
|
driveRegex := regexp.MustCompile(`^Drive\s+(\d+):`)
|
|
|
|
maxSlot := 0
|
|
driveCount = 0
|
|
|
|
for scanner.Scan() {
|
|
line := strings.TrimSpace(scanner.Text())
|
|
if strings.HasPrefix(line, "#") || line == "" {
|
|
continue
|
|
}
|
|
|
|
if matches := slotRegex.FindStringSubmatch(line); matches != nil {
|
|
slot, _ := strconv.Atoi(matches[1])
|
|
if slot > maxSlot {
|
|
maxSlot = slot
|
|
}
|
|
}
|
|
if matches := driveRegex.FindStringSubmatch(line); matches != nil {
|
|
driveCount++
|
|
}
|
|
}
|
|
|
|
slotCount = maxSlot
|
|
if slotCount == 0 {
|
|
slotCount = 10 // Default
|
|
}
|
|
if driveCount == 0 {
|
|
driveCount = 2 // Default
|
|
}
|
|
|
|
return slotCount, driveCount
|
|
}
|