mirror of
https://github.com/kairos-io/kairos-sdk.git
synced 2025-04-28 03:20:52 +00:00
331 lines
11 KiB
Go
331 lines
11 KiB
Go
package ghw
|
|
|
|
import (
|
|
"bufio"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/kairos-io/kairos-sdk/types"
|
|
)
|
|
|
|
const (
|
|
sectorSize = 512
|
|
UNKNOWN = "unknown"
|
|
)
|
|
|
|
type Paths struct {
|
|
SysBlock string
|
|
RunUdevData string
|
|
ProcMounts string
|
|
}
|
|
|
|
func NewPaths(withOptionalPrefix string) *Paths {
|
|
p := &Paths{
|
|
SysBlock: "/sys/block/",
|
|
RunUdevData: "/run/udev/data",
|
|
ProcMounts: "/proc/mounts",
|
|
}
|
|
|
|
// Allow overriding the paths via env var. It has precedence over anything
|
|
val, exists := os.LookupEnv("GHW_CHROOT")
|
|
if exists {
|
|
val = strings.TrimSuffix(val, "/")
|
|
p.SysBlock = fmt.Sprintf("%s%s", val, p.SysBlock)
|
|
p.RunUdevData = fmt.Sprintf("%s%s", val, p.RunUdevData)
|
|
p.ProcMounts = fmt.Sprintf("%s%s", val, p.ProcMounts)
|
|
return p
|
|
}
|
|
|
|
if withOptionalPrefix != "" {
|
|
withOptionalPrefix = strings.TrimSuffix(withOptionalPrefix, "/")
|
|
p.SysBlock = fmt.Sprintf("%s%s", withOptionalPrefix, p.SysBlock)
|
|
p.RunUdevData = fmt.Sprintf("%s%s", withOptionalPrefix, p.RunUdevData)
|
|
p.ProcMounts = fmt.Sprintf("%s%s", withOptionalPrefix, p.ProcMounts)
|
|
}
|
|
return p
|
|
}
|
|
|
|
func GetDisks(paths *Paths, logger *types.KairosLogger) []*types.Disk {
|
|
if logger == nil {
|
|
newLogger := types.NewKairosLogger("ghw", "info", false)
|
|
logger = &newLogger
|
|
}
|
|
disks := make([]*types.Disk, 0)
|
|
logger.Logger.Debug().Str("path", paths.SysBlock).Msg("Scanning for disks")
|
|
files, err := os.ReadDir(paths.SysBlock)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
for _, file := range files {
|
|
logger.Logger.Debug().Str("file", file.Name()).Msg("Reading file")
|
|
dname := file.Name()
|
|
size := diskSizeBytes(paths, dname, logger)
|
|
|
|
if strings.HasPrefix(dname, "loop") && size == 0 {
|
|
// We don't care about unused loop devices...
|
|
continue
|
|
}
|
|
d := &types.Disk{
|
|
Name: dname,
|
|
SizeBytes: size,
|
|
UUID: diskUUID(paths, dname, "", logger),
|
|
}
|
|
|
|
parts := diskPartitions(paths, dname, logger)
|
|
d.Partitions = parts
|
|
|
|
disks = append(disks, d)
|
|
}
|
|
|
|
return disks
|
|
}
|
|
|
|
func diskSizeBytes(paths *Paths, disk string, logger *types.KairosLogger) uint64 {
|
|
// We can find the number of 512-byte sectors by examining the contents of
|
|
// /sys/block/$DEVICE/size and calculate the physical bytes accordingly.
|
|
path := filepath.Join(paths.SysBlock, disk, "size")
|
|
logger.Logger.Debug().Str("path", path).Msg("Reading disk size")
|
|
contents, err := os.ReadFile(path)
|
|
if err != nil {
|
|
logger.Logger.Error().Str("path", path).Err(err).Msg("Failed to read file")
|
|
return 0
|
|
}
|
|
size, err := strconv.ParseUint(strings.TrimSpace(string(contents)), 10, 64)
|
|
if err != nil {
|
|
logger.Logger.Error().Str("path", path).Err(err).Str("content", string(contents)).Msg("Failed to parse size")
|
|
return 0
|
|
}
|
|
logger.Logger.Trace().Uint64("size", size*sectorSize).Msg("Got disk size")
|
|
return size * sectorSize
|
|
}
|
|
|
|
// diskPartitions takes the name of a disk (note: *not* the path of the disk,
|
|
// but just the name. In other words, "sda", not "/dev/sda" and "nvme0n1" not
|
|
// "/dev/nvme0n1") and returns a slice of pointers to Partition structs
|
|
// representing the partitions in that disk
|
|
func diskPartitions(paths *Paths, disk string, logger *types.KairosLogger) types.PartitionList {
|
|
out := make(types.PartitionList, 0)
|
|
path := filepath.Join(paths.SysBlock, disk)
|
|
logger.Logger.Debug().Str("file", path).Msg("Reading disk file")
|
|
files, err := os.ReadDir(path)
|
|
if err != nil {
|
|
logger.Logger.Error().Err(err).Msg("failed to read disk partitions")
|
|
return out
|
|
}
|
|
for _, file := range files {
|
|
fname := file.Name()
|
|
if !strings.HasPrefix(fname, disk) {
|
|
continue
|
|
}
|
|
logger.Logger.Debug().Str("file", fname).Msg("Reading partition file")
|
|
size := partitionSizeBytes(paths, disk, fname, logger)
|
|
mp, pt := partitionInfo(paths, fname, logger)
|
|
du := diskPartUUID(paths, disk, fname, logger)
|
|
if pt == "" {
|
|
pt = diskPartTypeUdev(paths, disk, fname, logger)
|
|
}
|
|
fsLabel := diskFSLabel(paths, disk, fname, logger)
|
|
p := &types.Partition{
|
|
Name: fname,
|
|
Size: uint(size / (1024 * 1024)),
|
|
MountPoint: mp,
|
|
UUID: du,
|
|
FilesystemLabel: fsLabel,
|
|
FS: pt,
|
|
Path: filepath.Join("/dev", fname),
|
|
Disk: filepath.Join("/dev", disk),
|
|
}
|
|
out = append(out, p)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func partitionSizeBytes(paths *Paths, disk string, part string, logger *types.KairosLogger) uint64 {
|
|
path := filepath.Join(paths.SysBlock, disk, part, "size")
|
|
logger.Logger.Debug().Str("file", path).Msg("Reading size file")
|
|
contents, err := os.ReadFile(path)
|
|
if err != nil {
|
|
logger.Logger.Error().Str("file", path).Err(err).Msg("failed to read disk partition size")
|
|
return 0
|
|
}
|
|
size, err := strconv.ParseUint(strings.TrimSpace(string(contents)), 10, 64)
|
|
if err != nil {
|
|
logger.Logger.Error().Str("contents", string(contents)).Err(err).Msg("failed to parse disk partition size")
|
|
return 0
|
|
}
|
|
logger.Logger.Trace().Str("disk", disk).Str("partition", part).Uint64("size", size*sectorSize).Msg("Got partition size")
|
|
return size * sectorSize
|
|
}
|
|
|
|
func partitionInfo(paths *Paths, part string, logger *types.KairosLogger) (string, string) {
|
|
// Allow calling PartitionInfo with either the full partition name
|
|
// "/dev/sda1" or just "sda1"
|
|
if !strings.HasPrefix(part, "/dev") {
|
|
part = "/dev/" + part
|
|
}
|
|
|
|
// mount entries for mounted partitions look like this:
|
|
// /dev/sda6 / ext4 rw,relatime,errors=remount-ro,data=ordered 0 0
|
|
var r io.ReadCloser
|
|
logger.Logger.Debug().Str("file", paths.ProcMounts).Msg("Reading mounts file")
|
|
r, err := os.Open(paths.ProcMounts)
|
|
if err != nil {
|
|
logger.Logger.Error().Str("file", paths.ProcMounts).Err(err).Msg("failed to open mounts")
|
|
return "", ""
|
|
}
|
|
defer r.Close()
|
|
|
|
scanner := bufio.NewScanner(r)
|
|
for scanner.Scan() {
|
|
line := scanner.Text()
|
|
logger.Logger.Trace().Str("line", line).Msg("Parsing mount info")
|
|
entry := parseMountEntry(line, logger)
|
|
if entry == nil || entry.Partition != part {
|
|
continue
|
|
}
|
|
|
|
return entry.Mountpoint, entry.FilesystemType
|
|
}
|
|
return "", ""
|
|
}
|
|
|
|
type mountEntry struct {
|
|
Partition string
|
|
Mountpoint string
|
|
FilesystemType string
|
|
}
|
|
|
|
func parseMountEntry(line string, logger *types.KairosLogger) *mountEntry {
|
|
// mount entries for mounted partitions look like this:
|
|
// /dev/sda6 / ext4 rw,relatime,errors=remount-ro,data=ordered 0 0
|
|
if line[0] != '/' {
|
|
return nil
|
|
}
|
|
fields := strings.Fields(line)
|
|
|
|
if len(fields) < 4 {
|
|
logger.Logger.Debug().Interface("fields", fields).Msg("Mount line has less than 4 fields")
|
|
return nil
|
|
}
|
|
|
|
// We do some special parsing of the mountpoint, which may contain space,
|
|
// tab and newline characters, encoded into the mount entry line using their
|
|
// octal-to-string representations. From the GNU mtab man pages:
|
|
//
|
|
// "Therefore these characters are encoded in the files and the getmntent
|
|
// function takes care of the decoding while reading the entries back in.
|
|
// '\040' is used to encode a space character, '\011' to encode a tab
|
|
// character, '\012' to encode a newline character, and '\\' to encode a
|
|
// backslash."
|
|
mp := fields[1]
|
|
r := strings.NewReplacer(
|
|
"\\011", "\t", "\\012", "\n", "\\040", " ", "\\\\", "\\",
|
|
)
|
|
mp = r.Replace(mp)
|
|
|
|
res := &mountEntry{
|
|
Partition: fields[0],
|
|
Mountpoint: mp,
|
|
FilesystemType: fields[2],
|
|
}
|
|
return res
|
|
}
|
|
|
|
func diskUUID(paths *Paths, disk string, partition string, logger *types.KairosLogger) string {
|
|
info, err := udevInfoPartition(paths, disk, partition, logger)
|
|
logger.Logger.Trace().Interface("info", info).Msg("Disk UUID")
|
|
if err != nil {
|
|
logger.Logger.Error().Str("disk", disk).Str("partition", partition).Interface("info", info).Err(err).Msg("failed to read disk UUID")
|
|
return UNKNOWN
|
|
}
|
|
|
|
if pType, ok := info["ID_PART_TABLE_UUID"]; ok {
|
|
logger.Logger.Trace().Str("disk", disk).Str("partition", partition).Str("uuid", pType).Msg("Got disk uuid")
|
|
return pType
|
|
}
|
|
|
|
return UNKNOWN
|
|
}
|
|
|
|
func diskPartUUID(paths *Paths, disk string, partition string, logger *types.KairosLogger) string {
|
|
info, err := udevInfoPartition(paths, disk, partition, logger)
|
|
logger.Logger.Trace().Interface("info", info).Msg("Disk Part UUID")
|
|
if err != nil {
|
|
logger.Logger.Error().Str("disk", disk).Str("partition", partition).Interface("info", info).Err(err).Msg("Disk Part UUID")
|
|
return UNKNOWN
|
|
}
|
|
|
|
if pType, ok := info["ID_PART_ENTRY_UUID"]; ok {
|
|
logger.Logger.Trace().Str("disk", disk).Str("partition", partition).Str("uuid", pType).Msg("Got partition uuid")
|
|
return pType
|
|
}
|
|
return UNKNOWN
|
|
}
|
|
|
|
// diskPartTypeUdev gets the partition type from the udev database directly and its only used as fallback when
|
|
// the partition is not mounted, so we cannot get the type from paths.ProcMounts from the partitionInfo function
|
|
func diskPartTypeUdev(paths *Paths, disk string, partition string, logger *types.KairosLogger) string {
|
|
info, err := udevInfoPartition(paths, disk, partition, logger)
|
|
logger.Logger.Trace().Interface("info", info).Msg("Disk Part Type")
|
|
if err != nil {
|
|
logger.Logger.Error().Str("disk", disk).Str("partition", partition).Interface("info", info).Err(err).Msg("Disk Part Type")
|
|
return UNKNOWN
|
|
}
|
|
|
|
if pType, ok := info["ID_FS_TYPE"]; ok {
|
|
logger.Logger.Trace().Str("disk", disk).Str("partition", partition).Str("FS", pType).Msg("Got partition fs type")
|
|
return pType
|
|
}
|
|
return UNKNOWN
|
|
}
|
|
|
|
func diskFSLabel(paths *Paths, disk string, partition string, logger *types.KairosLogger) string {
|
|
info, err := udevInfoPartition(paths, disk, partition, logger)
|
|
logger.Logger.Trace().Interface("info", info).Msg("Disk FS label")
|
|
if err != nil {
|
|
logger.Logger.Error().Str("disk", disk).Str("partition", partition).Interface("info", info).Err(err).Msg("Disk FS label")
|
|
return UNKNOWN
|
|
}
|
|
|
|
if label, ok := info["ID_FS_LABEL"]; ok {
|
|
logger.Logger.Trace().Str("disk", disk).Str("partition", partition).Str("uuid", label).Msg("Got partition label")
|
|
return label
|
|
}
|
|
return UNKNOWN
|
|
}
|
|
|
|
func udevInfoPartition(paths *Paths, disk string, partition string, logger *types.KairosLogger) (map[string]string, error) {
|
|
// Get device major:minor numbers
|
|
devNo, err := os.ReadFile(filepath.Join(paths.SysBlock, disk, partition, "dev"))
|
|
if err != nil {
|
|
logger.Logger.Error().Err(err).Str("path", filepath.Join(paths.SysBlock, disk, partition, "dev")).Msg("failed to read udev info")
|
|
return nil, err
|
|
}
|
|
return UdevInfo(paths, string(devNo), logger)
|
|
}
|
|
|
|
// UdevInfo will return information on udev database about a device number
|
|
func UdevInfo(paths *Paths, devNo string, logger *types.KairosLogger) (map[string]string, error) {
|
|
// Look up block device in udev runtime database
|
|
udevID := "b" + strings.TrimSpace(devNo)
|
|
udevBytes, err := os.ReadFile(filepath.Join(paths.RunUdevData, udevID))
|
|
if err != nil {
|
|
logger.Logger.Error().Err(err).Str("path", filepath.Join(paths.RunUdevData, udevID)).Msg("failed to read udev info for device")
|
|
return nil, err
|
|
}
|
|
|
|
udevInfo := make(map[string]string)
|
|
for _, udevLine := range strings.Split(string(udevBytes), "\n") {
|
|
if strings.HasPrefix(udevLine, "E:") {
|
|
if s := strings.SplitN(udevLine[2:], "=", 2); len(s) == 2 {
|
|
udevInfo[s[0]] = s[1]
|
|
}
|
|
}
|
|
}
|
|
return udevInfo, nil
|
|
}
|