- Installed and configured SCST with 7 handlers - Installed and configured mhVTL with 2 Quantum libraries and 8 LTO-8 drives - Implemented all VTL API endpoints (8/9 working) - Fixed NULL device_path handling in drives endpoint - Added comprehensive error handling and validation - Implemented async tape load/unload operations - Created SCST installation guide for Ubuntu 24.04 - Created mhVTL installation and configuration guide - Added VTL testing guide and automated test scripts - All core API tests passing (89% success rate) Infrastructure status: - PostgreSQL: Configured with proper permissions - SCST: Active with kernel module loaded - mhVTL: 2 libraries (Quantum Scalar i500, Scalar i40) - mhVTL: 8 drives (all Quantum ULTRIUM-HH8 LTO-8) - Calypso API: 8/9 VTL endpoints functional Documentation added: - src/srs-technical-spec-documents/scst-installation.md - src/srs-technical-spec-documents/mhvtl-installation.md - VTL-TESTING-GUIDE.md - scripts/test-vtl.sh Co-Authored-By: Warp <agent@warp.dev>
292 lines
8.8 KiB
Go
292 lines
8.8 KiB
Go
package storage
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"fmt"
|
|
"os/exec"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/atlasos/calypso/internal/common/database"
|
|
"github.com/atlasos/calypso/internal/common/logger"
|
|
)
|
|
|
|
// LVMService handles LVM operations
|
|
type LVMService struct {
|
|
db *database.DB
|
|
logger *logger.Logger
|
|
}
|
|
|
|
// NewLVMService creates a new LVM service
|
|
func NewLVMService(db *database.DB, log *logger.Logger) *LVMService {
|
|
return &LVMService{
|
|
db: db,
|
|
logger: log,
|
|
}
|
|
}
|
|
|
|
// VolumeGroup represents an LVM volume group
|
|
type VolumeGroup struct {
|
|
ID string `json:"id"`
|
|
Name string `json:"name"`
|
|
SizeBytes int64 `json:"size_bytes"`
|
|
FreeBytes int64 `json:"free_bytes"`
|
|
PhysicalVolumes []string `json:"physical_volumes"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
}
|
|
|
|
// Repository represents a disk repository (logical volume)
|
|
type Repository struct {
|
|
ID string `json:"id"`
|
|
Name string `json:"name"`
|
|
Description string `json:"description"`
|
|
VolumeGroup string `json:"volume_group"`
|
|
LogicalVolume string `json:"logical_volume"`
|
|
SizeBytes int64 `json:"size_bytes"`
|
|
UsedBytes int64 `json:"used_bytes"`
|
|
FilesystemType string `json:"filesystem_type"`
|
|
MountPoint string `json:"mount_point"`
|
|
IsActive bool `json:"is_active"`
|
|
WarningThresholdPercent int `json:"warning_threshold_percent"`
|
|
CriticalThresholdPercent int `json:"critical_threshold_percent"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
CreatedBy string `json:"created_by"`
|
|
}
|
|
|
|
// ListVolumeGroups lists all volume groups
|
|
func (s *LVMService) ListVolumeGroups(ctx context.Context) ([]VolumeGroup, error) {
|
|
cmd := exec.CommandContext(ctx, "vgs", "--units=b", "--noheadings", "--nosuffix", "-o", "vg_name,vg_size,vg_free,pv_name")
|
|
output, err := cmd.Output()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to list volume groups: %w", err)
|
|
}
|
|
|
|
vgMap := make(map[string]*VolumeGroup)
|
|
lines := strings.Split(strings.TrimSpace(string(output)), "\n")
|
|
|
|
for _, line := range lines {
|
|
if line == "" {
|
|
continue
|
|
}
|
|
fields := strings.Fields(line)
|
|
if len(fields) < 3 {
|
|
continue
|
|
}
|
|
|
|
vgName := fields[0]
|
|
vgSize, _ := strconv.ParseInt(fields[1], 10, 64)
|
|
vgFree, _ := strconv.ParseInt(fields[2], 10, 64)
|
|
pvName := ""
|
|
if len(fields) > 3 {
|
|
pvName = fields[3]
|
|
}
|
|
|
|
if vg, exists := vgMap[vgName]; exists {
|
|
if pvName != "" {
|
|
vg.PhysicalVolumes = append(vg.PhysicalVolumes, pvName)
|
|
}
|
|
} else {
|
|
vgMap[vgName] = &VolumeGroup{
|
|
Name: vgName,
|
|
SizeBytes: vgSize,
|
|
FreeBytes: vgFree,
|
|
PhysicalVolumes: []string{},
|
|
}
|
|
if pvName != "" {
|
|
vgMap[vgName].PhysicalVolumes = append(vgMap[vgName].PhysicalVolumes, pvName)
|
|
}
|
|
}
|
|
}
|
|
|
|
var vgs []VolumeGroup
|
|
for _, vg := range vgMap {
|
|
vgs = append(vgs, *vg)
|
|
}
|
|
|
|
return vgs, nil
|
|
}
|
|
|
|
// CreateRepository creates a new repository (logical volume)
|
|
func (s *LVMService) CreateRepository(ctx context.Context, name, vgName string, sizeBytes int64, createdBy string) (*Repository, error) {
|
|
// Generate logical volume name
|
|
lvName := "calypso-" + name
|
|
|
|
// Create logical volume
|
|
cmd := exec.CommandContext(ctx, "lvcreate", "-L", fmt.Sprintf("%dB", sizeBytes), "-n", lvName, vgName)
|
|
output, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create logical volume: %s: %w", string(output), err)
|
|
}
|
|
|
|
// Get device path
|
|
devicePath := fmt.Sprintf("/dev/%s/%s", vgName, lvName)
|
|
|
|
// Create filesystem (XFS)
|
|
cmd = exec.CommandContext(ctx, "mkfs.xfs", "-f", devicePath)
|
|
output, err = cmd.CombinedOutput()
|
|
if err != nil {
|
|
// Cleanup: remove LV if filesystem creation fails
|
|
exec.CommandContext(ctx, "lvremove", "-f", fmt.Sprintf("%s/%s", vgName, lvName)).Run()
|
|
return nil, fmt.Errorf("failed to create filesystem: %s: %w", string(output), err)
|
|
}
|
|
|
|
// Insert into database
|
|
query := `
|
|
INSERT INTO disk_repositories (
|
|
name, volume_group, logical_volume, size_bytes, used_bytes,
|
|
filesystem_type, is_active, created_by
|
|
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
|
RETURNING id, created_at, updated_at
|
|
`
|
|
|
|
var repo Repository
|
|
err = s.db.QueryRowContext(ctx, query,
|
|
name, vgName, lvName, sizeBytes, 0, "xfs", true, createdBy,
|
|
).Scan(&repo.ID, &repo.CreatedAt, &repo.UpdatedAt)
|
|
if err != nil {
|
|
// Cleanup: remove LV if database insert fails
|
|
exec.CommandContext(ctx, "lvremove", "-f", fmt.Sprintf("%s/%s", vgName, lvName)).Run()
|
|
return nil, fmt.Errorf("failed to save repository to database: %w", err)
|
|
}
|
|
|
|
repo.Name = name
|
|
repo.VolumeGroup = vgName
|
|
repo.LogicalVolume = lvName
|
|
repo.SizeBytes = sizeBytes
|
|
repo.UsedBytes = 0
|
|
repo.FilesystemType = "xfs"
|
|
repo.IsActive = true
|
|
repo.WarningThresholdPercent = 80
|
|
repo.CriticalThresholdPercent = 90
|
|
repo.CreatedBy = createdBy
|
|
|
|
s.logger.Info("Repository created", "name", name, "size_bytes", sizeBytes)
|
|
return &repo, nil
|
|
}
|
|
|
|
// GetRepository retrieves a repository by ID
|
|
func (s *LVMService) GetRepository(ctx context.Context, id string) (*Repository, error) {
|
|
query := `
|
|
SELECT id, name, description, volume_group, logical_volume,
|
|
size_bytes, used_bytes, filesystem_type, mount_point,
|
|
is_active, warning_threshold_percent, critical_threshold_percent,
|
|
created_at, updated_at, created_by
|
|
FROM disk_repositories
|
|
WHERE id = $1
|
|
`
|
|
|
|
var repo Repository
|
|
err := s.db.QueryRowContext(ctx, query, id).Scan(
|
|
&repo.ID, &repo.Name, &repo.Description, &repo.VolumeGroup,
|
|
&repo.LogicalVolume, &repo.SizeBytes, &repo.UsedBytes,
|
|
&repo.FilesystemType, &repo.MountPoint, &repo.IsActive,
|
|
&repo.WarningThresholdPercent, &repo.CriticalThresholdPercent,
|
|
&repo.CreatedAt, &repo.UpdatedAt, &repo.CreatedBy,
|
|
)
|
|
if err != nil {
|
|
if err == sql.ErrNoRows {
|
|
return nil, fmt.Errorf("repository not found")
|
|
}
|
|
return nil, fmt.Errorf("failed to get repository: %w", err)
|
|
}
|
|
|
|
// Update used bytes from actual filesystem
|
|
s.updateRepositoryUsage(ctx, &repo)
|
|
|
|
return &repo, nil
|
|
}
|
|
|
|
// ListRepositories lists all repositories
|
|
func (s *LVMService) ListRepositories(ctx context.Context) ([]Repository, error) {
|
|
query := `
|
|
SELECT id, name, description, volume_group, logical_volume,
|
|
size_bytes, used_bytes, filesystem_type, mount_point,
|
|
is_active, warning_threshold_percent, critical_threshold_percent,
|
|
created_at, updated_at, created_by
|
|
FROM disk_repositories
|
|
ORDER BY name
|
|
`
|
|
|
|
rows, err := s.db.QueryContext(ctx, query)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to list repositories: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
var repos []Repository
|
|
for rows.Next() {
|
|
var repo Repository
|
|
err := rows.Scan(
|
|
&repo.ID, &repo.Name, &repo.Description, &repo.VolumeGroup,
|
|
&repo.LogicalVolume, &repo.SizeBytes, &repo.UsedBytes,
|
|
&repo.FilesystemType, &repo.MountPoint, &repo.IsActive,
|
|
&repo.WarningThresholdPercent, &repo.CriticalThresholdPercent,
|
|
&repo.CreatedAt, &repo.UpdatedAt, &repo.CreatedBy,
|
|
)
|
|
if err != nil {
|
|
s.logger.Error("Failed to scan repository", "error", err)
|
|
continue
|
|
}
|
|
|
|
// Update used bytes from actual filesystem
|
|
s.updateRepositoryUsage(ctx, &repo)
|
|
repos = append(repos, repo)
|
|
}
|
|
|
|
return repos, rows.Err()
|
|
}
|
|
|
|
// updateRepositoryUsage updates repository usage from filesystem
|
|
func (s *LVMService) updateRepositoryUsage(ctx context.Context, repo *Repository) {
|
|
// Use df to get filesystem usage (if mounted)
|
|
// For now, use lvs to get actual size
|
|
cmd := exec.CommandContext(ctx, "lvs", "--units=b", "--noheadings", "--nosuffix", "-o", "lv_size,data_percent", fmt.Sprintf("%s/%s", repo.VolumeGroup, repo.LogicalVolume))
|
|
output, err := cmd.Output()
|
|
if err == nil {
|
|
fields := strings.Fields(string(output))
|
|
if len(fields) >= 1 {
|
|
if size, err := strconv.ParseInt(fields[0], 10, 64); err == nil {
|
|
repo.SizeBytes = size
|
|
}
|
|
}
|
|
}
|
|
|
|
// Update in database
|
|
s.db.ExecContext(ctx, `
|
|
UPDATE disk_repositories SET used_bytes = $1, updated_at = NOW() WHERE id = $2
|
|
`, repo.UsedBytes, repo.ID)
|
|
}
|
|
|
|
// DeleteRepository deletes a repository
|
|
func (s *LVMService) DeleteRepository(ctx context.Context, id string) error {
|
|
repo, err := s.GetRepository(ctx, id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if repo.IsActive {
|
|
return fmt.Errorf("cannot delete active repository")
|
|
}
|
|
|
|
// Remove logical volume
|
|
cmd := exec.CommandContext(ctx, "lvremove", "-f", fmt.Sprintf("%s/%s", repo.VolumeGroup, repo.LogicalVolume))
|
|
output, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to remove logical volume: %s: %w", string(output), err)
|
|
}
|
|
|
|
// Delete from database
|
|
_, err = s.db.ExecContext(ctx, "DELETE FROM disk_repositories WHERE id = $1", id)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to delete repository from database: %w", err)
|
|
}
|
|
|
|
s.logger.Info("Repository deleted", "id", id, "name", repo.Name)
|
|
return nil
|
|
}
|
|
|