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 1 commit
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
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
Loading