joshuar-go-hass-agent/internal/linux/disk/usageStats.go

157 lines
4.0 KiB
Go

// Copyright (c) 2024 Joshua Rich <joshua.rich@gmail.com>
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
package disk
import (
"bufio"
"bytes"
"context"
"fmt"
"log/slog"
"os"
"path/filepath"
"slices"
"strings"
"golang.org/x/sys/unix"
"github.com/joshuar/go-hass-agent/internal/linux"
"github.com/joshuar/go-hass-agent/internal/logging"
)
const (
mountAttrDevice = "device"
mountAttrFs = "filesystem_type"
mountAttrOpts = "mount_options"
mountAttBlockSize = "block_size"
mountAttrBlocksTotal = "blocks_total"
mountAttrBlocksFree = "blocks_free"
mountAttrBlocksAvail = "blocks_available"
mountAttrInodesTotal = "inodes_total"
mountAttrInodesFree = "inodes_free"
)
type mount struct {
attributes map[string]any
mountpoint string
}
var (
validVirtualFs = []string{"tmpfs", "ramfs", "cifs", "smb", "nfs"}
mountBlocklist = []string{"/tmp/crun", "/run"}
)
func (m *mount) getMountInfo() error {
var stats unix.Statfs_t
err := unix.Statfs(m.mountpoint, &stats)
if err != nil {
return fmt.Errorf("getMountInfo: %w", err)
}
m.attributes[mountAttBlockSize] = stats.Bsize
m.attributes[mountAttrBlocksTotal] = stats.Blocks
m.attributes[mountAttrBlocksFree] = stats.Bfree
m.attributes[mountAttrBlocksAvail] = stats.Bavail
m.attributes[mountAttrInodesTotal] = stats.Files
m.attributes[mountAttrInodesFree] = stats.Ffree
return nil
}
func getFilesystems() ([]string, error) {
data, err := os.Open(filepath.Join(linux.ProcFSRoot, "filesystems"))
if err != nil {
return nil, fmt.Errorf("getFilesystems: %w", err)
}
defer data.Close()
var filesystems []string
// Scan each line.
entry := bufio.NewScanner(data)
for entry.Scan() {
line := bufio.NewScanner(bytes.NewReader(entry.Bytes()))
line.Split(bufio.ScanWords)
// Scan fields of line.
for line.Scan() {
switch value := line.Text(); value {
case "nodev": // Is virtual filesystem. Check second field for fs.
line.Scan()
// If one of validVirtualFs, add it to tracked filesystems.
if slices.Contains(validVirtualFs, line.Text()) {
filesystems = append(filesystems, line.Text())
}
default: // Is block/regular fs. Add to tracked filesystems.
filesystems = append(filesystems, value)
}
}
}
return filesystems, nil
}
func getMounts(ctx context.Context) ([]*mount, error) {
// Get valid filesystems.
filesystems, err := getFilesystems()
if err != nil {
return nil, fmt.Errorf("getMounts: %w", err)
}
// Open mounts file.
data, err := os.Open(filepath.Join(linux.ProcFSRoot, "mounts"))
if err != nil {
return nil, fmt.Errorf("getMounts: %w", err)
}
var mounts []*mount
// Scan the file.
entry := bufio.NewScanner(data)
for entry.Scan() {
// Scan the line and extract first four fields device, mount, fs and
// opts respectively..
line := bufio.NewScanner(bytes.NewReader(entry.Bytes()))
line.Split(bufio.ScanWords)
line.Scan()
device := line.Text()
line.Scan()
mountpoint := line.Text()
line.Scan()
filesystem := line.Text()
line.Scan()
opts := line.Text()
// If the fs is in our valid filesystems, It should be in the list of
// valid filesystems and not one of the blocked mountpoints.
if slices.Contains(filesystems, filesystem) &&
!slices.ContainsFunc(mountBlocklist, func(blockedMount string) bool { return strings.HasPrefix(mountpoint, blockedMount) }) {
validmount := &mount{
mountpoint: mountpoint,
attributes: make(map[string]any),
}
validmount.attributes[mountAttrDevice] = device
validmount.attributes[mountAttrFs] = filesystem
validmount.attributes[mountAttrOpts] = opts
if err := validmount.getMountInfo(); err != nil {
logging.FromContext(ctx).
With(slog.String("worker", usageWorkerID)).
Debug("Error getting mount info.", slog.Any("error", err))
} else {
mounts = append(mounts, validmount)
}
}
}
if err := data.Close(); err != nil {
logging.FromContext(ctx).
With(slog.String("worker", usageWorkerID)).
Debug("Failed to close mounts file.", slog.Any("error", err))
}
return mounts, nil
}