Skip to content

Commit

Permalink
Bring GHW clone and partition type into sdk
Browse files Browse the repository at this point in the history
Centralize the Kairos Partition and the methods for getting partitions
from the system

Signed-off-by: Itxaka <[email protected]>
  • Loading branch information
Itxaka committed Sep 11, 2024
1 parent 8f40879 commit 182407c
Show file tree
Hide file tree
Showing 4 changed files with 307 additions and 60 deletions.
277 changes: 277 additions & 0 deletions ghw/ghw.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,277 @@
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"`
SizeBytes uint64 `json:"size_bytes"`
Partitions types.PartitionList `json:"partitions"`
}

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) []*Disk {
disks := make([]*Disk, 0)
files, err := os.ReadDir(paths.SysBlock)
if err != nil {
return nil
}
for _, file := range files {
dname := file.Name()
size := diskSizeBytes(paths, dname)

if strings.HasPrefix(dname, "loop") && size == 0 {
// We don't care about unused loop devices...
continue
}
d := &Disk{
Name: dname,
SizeBytes: size,
}

parts := diskPartitions(paths, dname)
d.Partitions = parts

disks = append(disks, d)
}

return disks
}

func diskSizeBytes(paths *Paths, disk string) 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")
contents, err := os.ReadFile(path)
if err != nil {
return 0
}
size, err := strconv.ParseUint(strings.TrimSpace(string(contents)), 10, 64)
if err != nil {
return 0
}
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) types.PartitionList {
out := make(types.PartitionList, 0)
path := filepath.Join(paths.SysBlock, disk)
files, err := os.ReadDir(path)
if err != nil {
fmt.Println("failed to read disk partitions: %s\n", err)
return out
}
for _, file := range files {
fname := file.Name()
if !strings.HasPrefix(fname, disk) {
continue
}
size := partitionSizeBytes(paths, disk, fname)
mp, pt := partitionInfo(paths, fname)
du := diskPartUUID(paths, disk, fname)
if pt == "" {
pt = diskPartTypeUdev(paths, disk, fname)
}
fsLabel := diskFSLabel(paths, disk, fname)
p := &types.Partition{
Name: fname,
Size: uint(size / (1024 * 1024)),
MountPoint: mp,
UUID: du,
FilesystemLabel: fsLabel,
Type: 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) uint64 {
path := filepath.Join(paths.SysBlock, disk, part, "size")
contents, err := os.ReadFile(path)
if err != nil {
return 0
}
size, err := strconv.ParseUint(strings.TrimSpace(string(contents)), 10, 64)
if err != nil {
return 0
}
return size * sectorSize
}

func partitionInfo(paths *Paths, part string) (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
r, err := os.Open(paths.ProcMounts)
if err != nil {
return "", ""
}
defer r.Close()

scanner := bufio.NewScanner(r)
for scanner.Scan() {
line := scanner.Text()
entry := parseMountEntry(line)
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) *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 {
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 diskPartUUID(paths *Paths, disk string, partition string) string {
info, err := udevInfoPartition(paths, disk, partition)
if err != nil {
return UNKNOWN
}

if pType, ok := info["ID_PART_ENTRY_UUID"]; ok {
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) string {
info, err := udevInfoPartition(paths, disk, partition)
if err != nil {
return UNKNOWN
}

if pType, ok := info["ID_FS_TYPE"]; ok {
return pType
}
return UNKNOWN
}

func diskFSLabel(paths *Paths, disk string, partition string) string {
info, err := udevInfoPartition(paths, disk, partition)
if err != nil {
return UNKNOWN
}

if label, ok := info["ID_FS_LABEL"]; ok {
return label
}
return UNKNOWN
}

func udevInfoPartition(paths *Paths, disk string, partition string) (map[string]string, error) {
// Get device major:minor numbers
devNo, err := os.ReadFile(filepath.Join(paths.SysBlock, disk, partition, "dev"))
if err != nil {
return nil, err
}
return UdevInfo(paths, string(devNo))
}

// UdevInfo will return information on udev database about a device number
func UdevInfo(paths *Paths, devNo string) (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 {
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
}
7 changes: 2 additions & 5 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,7 @@ require (
github.com/qeesung/image2ascii v1.0.1
github.com/rs/zerolog v1.33.0
github.com/saferwall/pe v1.5.4
github.com/santhosh-tekuri/jsonschema/v6 v6.0.1
github.com/santhosh-tekuri/jsonschema/v6 v6.0.1
github.com/santhosh-tekuri/jsonschema/v5 v5.3.1
github.com/swaggest/jsonschema-go v0.3.62
github.com/twpayne/go-vfs/v4 v4.3.0
github.com/urfave/cli/v2 v2.27.4
Expand Down Expand Up @@ -57,7 +56,6 @@ require (
github.com/docker/go-connections v0.4.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/felixge/httpsnoop v1.0.3 // indirect
github.com/ghodss/yaml v1.0.0 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-ole/go-ole v1.2.6 // indirect
Expand All @@ -71,7 +69,7 @@ require (
github.com/gookit/color v1.5.3 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/itchyny/timefmt-go v0.1.6 // indirect
github.com/jaypipes/pcidb v1.0.0 // indirect
github.com/jaypipes/pcidb v1.0.1 // indirect
github.com/klauspost/compress v1.17.4 // indirect
github.com/kr/pretty v0.2.1 // indirect
github.com/kr/text v0.2.0 // indirect
Expand Down Expand Up @@ -103,7 +101,6 @@ require (
go.opentelemetry.io/otel/metric v1.21.0 // indirect
go.opentelemetry.io/otel/trace v1.21.0 // indirect
golang.org/x/crypto v0.26.0 // indirect
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect
golang.org/x/net v0.28.0 // indirect
golang.org/x/sync v0.8.0 // indirect
golang.org/x/sys v0.24.0 // indirect
Expand Down
Loading

0 comments on commit 182407c

Please sign in to comment.