mirror of
https://github.com/AlchemillaHQ/Sylve.git
synced 2026-06-26 02:45:10 +03:00
330 lines
8.1 KiB
Go
330 lines
8.1 KiB
Go
// SPDX-License-Identifier: BSD-2-Clause
|
|
//
|
|
// Copyright (c) 2025 The FreeBSD Foundation.
|
|
//
|
|
// This software was developed by Hayzam Sherif <hayzam@alchemilla.io>
|
|
// of Alchemilla Ventures Pvt. Ltd. <hello@alchemilla.io>,
|
|
// under sponsorship from the FreeBSD Foundation.
|
|
|
|
package disk
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
"sync"
|
|
"syscall"
|
|
|
|
"github.com/alchemillahq/gzfs"
|
|
diskServiceInterfaces "github.com/alchemillahq/sylve/internal/interfaces/services/disk"
|
|
zfsServiceInterfaces "github.com/alchemillahq/sylve/internal/interfaces/services/zfs"
|
|
"github.com/alchemillahq/sylve/internal/logger"
|
|
diskUtils "github.com/alchemillahq/sylve/pkg/disk"
|
|
"github.com/alchemillahq/sylve/pkg/utils"
|
|
|
|
"github.com/rs/zerolog"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
var _ diskServiceInterfaces.DiskServiceInterface = (*Service)(nil)
|
|
|
|
type Service struct {
|
|
DB *gorm.DB
|
|
DiskOperationMutex sync.Mutex
|
|
ZFS zfsServiceInterfaces.ZfsServiceInterface
|
|
GZFS *gzfs.Client
|
|
}
|
|
|
|
func NewDiskService(db *gorm.DB, zfsService zfsServiceInterfaces.ZfsServiceInterface, gzfs *gzfs.Client) diskServiceInterfaces.DiskServiceInterface {
|
|
return &Service{
|
|
DB: db,
|
|
ZFS: zfsService,
|
|
GZFS: gzfs,
|
|
}
|
|
}
|
|
|
|
func findClassByName(mesh *diskServiceInterfaces.Mesh, name string) *diskServiceInterfaces.Class {
|
|
for i := range mesh.Classes {
|
|
if mesh.Classes[i].Name == name {
|
|
return &mesh.Classes[i]
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func ExtractDiskInfo(mesh *diskServiceInterfaces.Mesh) ([]diskServiceInterfaces.DiskInfo, error) {
|
|
if mesh == nil {
|
|
return nil, fmt.Errorf("nil mesh provided")
|
|
}
|
|
|
|
var disks []diskServiceInterfaces.DiskInfo
|
|
diskClass := findClassByName(mesh, "DISK")
|
|
if diskClass == nil {
|
|
return nil, fmt.Errorf("DISK class not found in mesh")
|
|
}
|
|
|
|
partClass := findClassByName(mesh, "PART")
|
|
if partClass == nil {
|
|
return nil, fmt.Errorf("PART class not found in mesh")
|
|
}
|
|
|
|
for _, geom := range diskClass.Geoms {
|
|
if len(geom.Providers) == 0 {
|
|
continue
|
|
}
|
|
|
|
provider := geom.Providers[0]
|
|
diskType := "HDD"
|
|
|
|
if provider.Config.RotationRate == "0" {
|
|
if strings.HasPrefix(provider.Name, "nvme") ||
|
|
strings.HasPrefix(provider.Name, "nda") ||
|
|
strings.HasPrefix(provider.Alias, "nv") {
|
|
diskType = "NVMe"
|
|
} else {
|
|
diskType = "SSD"
|
|
}
|
|
}
|
|
|
|
disk := diskServiceInterfaces.DiskInfo{
|
|
Name: provider.Name,
|
|
Aliases: []string{},
|
|
MediaSize: provider.MediaSize,
|
|
SectorSize: provider.SectorSize,
|
|
Description: provider.Config.Descr,
|
|
RotationRate: provider.Config.RotationRate,
|
|
Serial: provider.Config.Ident,
|
|
LunID: provider.Config.LunID,
|
|
Type: diskType,
|
|
Partitions: []diskServiceInterfaces.PartitionInfo{},
|
|
IsBootDevice: false,
|
|
}
|
|
|
|
if provider.Alias != "" {
|
|
disk.Aliases = append(disk.Aliases, provider.Alias)
|
|
}
|
|
|
|
for _, partGeom := range partClass.Geoms {
|
|
if partGeom.Name == provider.Name {
|
|
isGPT := false
|
|
if partGeom.Config.Scheme == "GPT" {
|
|
isGPT = true
|
|
}
|
|
|
|
for _, partProvider := range partGeom.Providers {
|
|
partition := diskServiceInterfaces.PartitionInfo{
|
|
Name: partProvider.Name,
|
|
Aliases: []string{},
|
|
Type: partProvider.Config.Type,
|
|
Label: partProvider.Config.Label,
|
|
Size: partProvider.Config.Length,
|
|
StartBlock: partProvider.Config.Start,
|
|
EndBlock: partProvider.Config.End,
|
|
UUID: partProvider.Config.RawUUID,
|
|
Filesystem: utils.GetDiskTypeFromUUID(partProvider.Config.RawType, partProvider.Config.Type),
|
|
GPT: isGPT,
|
|
}
|
|
|
|
if partProvider.Alias != "" {
|
|
partition.Aliases = append(partition.Aliases, partProvider.Alias)
|
|
}
|
|
|
|
if strings.Contains(partition.Type, "boot") || strings.Contains(partition.Type, "efi") {
|
|
disk.IsBootDevice = true
|
|
}
|
|
|
|
disk.Partitions = append(disk.Partitions, partition)
|
|
}
|
|
}
|
|
}
|
|
|
|
disks = append(disks, disk)
|
|
}
|
|
|
|
return disks, nil
|
|
}
|
|
|
|
func (s *Service) GetDiskDevices(ctx context.Context) ([]diskServiceInterfaces.Disk, error) {
|
|
var disks []diskServiceInterfaces.Disk
|
|
|
|
mesh, err := s.ParseGeomOutput()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
dinfo, err := ExtractDiskInfo(&mesh)
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for _, d := range dinfo {
|
|
var disk diskServiceInterfaces.Disk
|
|
disk.UUID = utils.GenerateDeterministicUUID(fmt.Sprintf("%s-%s", d.LunID, d.Serial))
|
|
disk.Device = d.Name
|
|
disk.Type = d.Type
|
|
disk.Size = uint64(d.MediaSize)
|
|
disk.Serial = d.Serial
|
|
|
|
if s.IsDiskGPT("/dev/" + d.Name) {
|
|
disk.GPT = true
|
|
} else {
|
|
disk.GPT = false
|
|
}
|
|
|
|
if d.Type == "NVMe" || d.Type == "SSD" || d.Type == "HDD" {
|
|
smartData, err := s.GetSmartData(d)
|
|
if err != nil {
|
|
logger.LogWithDeduplication(zerolog.DebugLevel, fmt.Sprintf("Failed to retrieve S.M.A.R.T data %v", err))
|
|
disk.SmartData = nil
|
|
} else if err == nil && smartData != nil {
|
|
disk.SmartData = smartData
|
|
|
|
}
|
|
} else {
|
|
disk.SmartData = nil
|
|
}
|
|
|
|
if d.Type == "NVMe" || d.Type == "SSD" || d.Type == "HDD" {
|
|
wearOut, err := s.GetWearOut(disk.SmartData)
|
|
if err != nil {
|
|
disk.WearOut = "Unknown"
|
|
} else {
|
|
disk.WearOut = fmt.Sprintf("%.2f", wearOut)
|
|
}
|
|
} else {
|
|
disk.WearOut = "Unknown"
|
|
}
|
|
|
|
disk.Partitions = []diskServiceInterfaces.Partition{}
|
|
|
|
disk.Model = d.Description
|
|
for _, p := range d.Partitions {
|
|
if strings.HasPrefix(p.Name, d.Name) {
|
|
var partition diskServiceInterfaces.Partition
|
|
partition.UUID = p.UUID
|
|
partition.Name = p.Name
|
|
partition.Usage = p.Filesystem
|
|
partition.Size = uint64(p.Size)
|
|
|
|
disk.Partitions = append(disk.Partitions, partition)
|
|
}
|
|
}
|
|
|
|
if len(disk.Partitions) == 0 {
|
|
found := false
|
|
devPath := "/dev/" + d.Name
|
|
|
|
in, _, err := s.GZFS.Zpool.IsDeviceInZpool(ctx, devPath)
|
|
if err == nil && in {
|
|
disk.Usage = "ZFS"
|
|
found = true
|
|
}
|
|
|
|
if !found {
|
|
disk.Usage = "Unused"
|
|
}
|
|
} else {
|
|
disk.Usage = "Partitions"
|
|
}
|
|
|
|
disks = append(disks, disk)
|
|
}
|
|
|
|
return disks, nil
|
|
}
|
|
|
|
func (s *Service) GetDiskSize(device string) (uint64, error) {
|
|
size, err := diskUtils.GetDiskSize(device)
|
|
|
|
if err != nil {
|
|
return 0, fmt.Errorf("failed to determine disk size: %v", err)
|
|
}
|
|
|
|
return size, nil
|
|
}
|
|
|
|
func (s *Service) DestroyPartitionTable(device string) error {
|
|
s.DiskOperationMutex.Lock()
|
|
defer s.DiskOperationMutex.Unlock()
|
|
|
|
if _, err := os.Stat(device); os.IsNotExist(err) {
|
|
return fmt.Errorf("device does not exist: %v", err)
|
|
}
|
|
|
|
file, err := os.OpenFile(device, os.O_WRONLY, 0600)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to open disk: %v", err)
|
|
}
|
|
|
|
defer file.Close()
|
|
|
|
diskSize, err := s.GetDiskSize(device)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get disk size: %v", err)
|
|
}
|
|
|
|
const wipeSize = 1024 * 1024
|
|
buffer := make([]byte, wipeSize)
|
|
|
|
_, err = file.WriteAt(buffer, 0)
|
|
if err != nil {
|
|
return fmt.Errorf("error wiping primary GPT: %v", err)
|
|
}
|
|
|
|
if diskSize > wipeSize {
|
|
_, err = file.WriteAt(buffer, int64(diskSize)-int64(wipeSize))
|
|
if err != nil {
|
|
return fmt.Errorf("error wiping backup GPT: %v", err)
|
|
}
|
|
} else {
|
|
return fmt.Errorf("disk size is too small for GPT")
|
|
}
|
|
|
|
err = syscall.Fsync(int(file.Fd()))
|
|
if err != nil {
|
|
return fmt.Errorf("failed to sync disk: %v", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *Service) InitializeGPT(device string) error {
|
|
s.DiskOperationMutex.Lock()
|
|
defer s.DiskOperationMutex.Unlock()
|
|
|
|
output, err := utils.RunCommand("/sbin/gpart", "create", "-s", "gpt", device)
|
|
if err != nil {
|
|
if strings.Contains(output, "File exists") {
|
|
return fmt.Errorf("gpt_partition_table_already_exists")
|
|
}
|
|
|
|
return fmt.Errorf("failed_to_create_gpt_partition_table %s", output)
|
|
}
|
|
|
|
baseDevice := strings.TrimPrefix(device, "/dev/")
|
|
expectedOutput := fmt.Sprintf("%s created", baseDevice)
|
|
|
|
if !strings.Contains(output, expectedOutput) {
|
|
return fmt.Errorf("failed_to_create_gpt_partition_table %s", output)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *Service) IsDiskGPT(device string) bool {
|
|
gptSector, err := utils.ReadDiskSector(device, 1)
|
|
if err != nil {
|
|
if strings.Contains(err.Error(), "device not configured") {
|
|
return false
|
|
}
|
|
|
|
logger.LogWithDeduplication(zerolog.DebugLevel, fmt.Sprintf("failed to read sector 1: %v", err))
|
|
|
|
return false
|
|
}
|
|
|
|
return utils.IsGPT(gptSector)
|
|
}
|