Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bring GHW clone and partition type into sdk #492

Merged
merged 4 commits into from
Sep 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
326 changes: 326 additions & 0 deletions ghw/ghw.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,326 @@
package ghw

import (
"bufio"
"fmt"
"io"
"os"
"path/filepath"
"strconv"
"strings"

"github.com/kairos-io/kairos-sdk/types"
)

const (
sectorSize = 512
UNKNOWN = "unknown"
)

type Disk struct {
Name string `json:"name,omitempty" yaml:"name,omitempty"`
SizeBytes uint64 `json:"size_bytes,omitempty" yaml:"size_bytes,omitempty"`
UUID string `json:"uuid,omitempty" yaml:"uuid,omitempty"`
Partitions types.PartitionList `json:"partitions,omitempty" yaml:"partitions,omitempty"`
}

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",
}
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) []*Disk {
if logger == nil {
newLogger := types.NewKairosLogger("ghw", "info", false)
logger = &newLogger
}
disks := make([]*Disk, 0)
logger.Logger.Debug().Str("path", paths.SysBlock).Msg("Scanning for disks")
files, err := os.ReadDir(paths.SysBlock)
if err != nil {
return nil

Check warning on line 57 in ghw/ghw.go

View check run for this annotation

Codecov / codecov/patch

ghw/ghw.go#L57

Added line #L57 was not covered by tests
}
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

Check warning on line 66 in ghw/ghw.go

View check run for this annotation

Codecov / codecov/patch

ghw/ghw.go#L66

Added line #L66 was not covered by tests
}
d := &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

Check warning on line 91 in ghw/ghw.go

View check run for this annotation

Codecov / codecov/patch

ghw/ghw.go#L90-L91

Added lines #L90 - L91 were not covered by tests
}
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

Check warning on line 96 in ghw/ghw.go

View check run for this annotation

Codecov / codecov/patch

ghw/ghw.go#L95-L96

Added lines #L95 - L96 were not covered by tests
}
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

Check warning on line 113 in ghw/ghw.go

View check run for this annotation

Codecov / codecov/patch

ghw/ghw.go#L112-L113

Added lines #L112 - L113 were not covered by tests
}
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)

Check warning on line 125 in ghw/ghw.go

View check run for this annotation

Codecov / codecov/patch

ghw/ghw.go#L125

Added line #L125 was not covered by tests
}
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

Check warning on line 149 in ghw/ghw.go

View check run for this annotation

Codecov / codecov/patch

ghw/ghw.go#L148-L149

Added lines #L148 - L149 were not covered by tests
}
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

Check warning on line 154 in ghw/ghw.go

View check run for this annotation

Codecov / codecov/patch

ghw/ghw.go#L153-L154

Added lines #L153 - L154 were not covered by tests
}
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 "", ""

Check warning on line 174 in ghw/ghw.go

View check run for this annotation

Codecov / codecov/patch

ghw/ghw.go#L173-L174

Added lines #L173 - L174 were not covered by tests
}
defer r.Close()

scanner := bufio.NewScanner(r)
for scanner.Scan() {
line := scanner.Text()
logger.Logger.Debug().Str("line", line).Msg("Parsing mount info")
entry := parseMountEntry(line, logger)
if entry == nil || entry.Partition != part {
continue

Check warning on line 184 in ghw/ghw.go

View check run for this annotation

Codecov / codecov/patch

ghw/ghw.go#L184

Added line #L184 was not covered by tests
}

return entry.Mountpoint, entry.FilesystemType
}
return "", ""

Check warning on line 189 in ghw/ghw.go

View check run for this annotation

Codecov / codecov/patch

ghw/ghw.go#L189

Added line #L189 was not covered by tests
}

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

Check warning on line 202 in ghw/ghw.go

View check run for this annotation

Codecov / codecov/patch

ghw/ghw.go#L202

Added line #L202 was not covered by tests
}
fields := strings.Fields(line)

if len(fields) < 4 {
logger.Logger.Debug().Interface("fields", fields).Msg("Mount line has less than 4 fields")
return nil

Check warning on line 208 in ghw/ghw.go

View check run for this annotation

Codecov / codecov/patch

ghw/ghw.go#L207-L208

Added lines #L207 - L208 were not covered by tests
}

// 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

Check warning on line 239 in ghw/ghw.go

View check run for this annotation

Codecov / codecov/patch

ghw/ghw.go#L238-L239

Added lines #L238 - L239 were not covered by tests
}

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

Check warning on line 247 in ghw/ghw.go

View check run for this annotation

Codecov / codecov/patch

ghw/ghw.go#L247

Added line #L247 was not covered by tests
}

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

Check warning on line 255 in ghw/ghw.go

View check run for this annotation

Codecov / codecov/patch

ghw/ghw.go#L254-L255

Added lines #L254 - L255 were not covered by tests
}

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

Check warning on line 262 in ghw/ghw.go

View check run for this annotation

Codecov / codecov/patch

ghw/ghw.go#L262

Added line #L262 was not covered by tests
}

// 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

Check warning on line 272 in ghw/ghw.go

View check run for this annotation

Codecov / codecov/patch

ghw/ghw.go#L267-L272

Added lines #L267 - L272 were not covered by tests
}

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

Check warning on line 277 in ghw/ghw.go

View check run for this annotation

Codecov / codecov/patch

ghw/ghw.go#L275-L277

Added lines #L275 - L277 were not covered by tests
}
return UNKNOWN

Check warning on line 279 in ghw/ghw.go

View check run for this annotation

Codecov / codecov/patch

ghw/ghw.go#L279

Added line #L279 was not covered by tests
}

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

Check warning on line 287 in ghw/ghw.go

View check run for this annotation

Codecov / codecov/patch

ghw/ghw.go#L286-L287

Added lines #L286 - L287 were not covered by tests
}

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

Check warning on line 294 in ghw/ghw.go

View check run for this annotation

Codecov / codecov/patch

ghw/ghw.go#L294

Added line #L294 was not covered by tests
}

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

Check warning on line 302 in ghw/ghw.go

View check run for this annotation

Codecov / codecov/patch

ghw/ghw.go#L301-L302

Added lines #L301 - L302 were not covered by tests
}
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

Check warning on line 314 in ghw/ghw.go

View check run for this annotation

Codecov / codecov/patch

ghw/ghw.go#L313-L314

Added lines #L313 - L314 were not covered by tests
}

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
}
Loading
Loading