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 }