kvm/internal/usbgadget/changeset.go

437 lines
13 KiB
Go

package usbgadget
import (
"bytes"
"fmt"
"os"
"path/filepath"
"reflect"
"time"
"github.com/prometheus/procfs"
"github.com/sourcegraph/tf-dag/dag"
)
// it's a minimalistic implementation of ansible's file module with some modifications
// to make it more suitable for our use case
// https://docs.ansible.com/ansible/latest/modules/file_module.html
// we use this to check if the files in the gadget config are in the expected state
// and to update them if they are not in the expected state
type FileState uint8
type ChangeState uint8
type FileChangeResolvedAction uint8
type ApplyFunc func(c *ChangeSet, changes []*FileChange) error
const (
FileStateUnknown FileState = iota
FileStateAbsent
FileStateDirectory
FileStateFile
FileStateFileContentMatch
FileStateFileWrite // update file content without checking
FileStateMounted
FileStateMountedConfigFS
FileStateSymlink
FileStateSymlinkInOrderConfigFS // configfs is a shithole, so we need to check if the symlinks are created in the correct order
FileStateSymlinkNotInOrderConfigFS
FileStateTouch
)
var FileStateString = map[FileState]string{
FileStateUnknown: "UNKNOWN",
FileStateAbsent: "ABSENT",
FileStateDirectory: "DIRECTORY",
FileStateFile: "FILE",
FileStateFileContentMatch: "FILE_CONTENT_MATCH",
FileStateFileWrite: "FILE_WRITE",
FileStateMounted: "MOUNTED",
FileStateMountedConfigFS: "CONFIGFS_MOUNTED",
FileStateSymlink: "SYMLINK",
FileStateSymlinkInOrderConfigFS: "SYMLINK_IN_ORDER_CONFIGFS",
FileStateTouch: "TOUCH",
}
const (
ChangeStateUnknown ChangeState = iota
ChangeStateRequired
ChangeStateNotChanged
ChangeStateChanged
ChangeStateError
)
const (
FileChangeResolvedActionUnknown FileChangeResolvedAction = iota
FileChangeResolvedActionDoNothing
FileChangeResolvedActionRemove
FileChangeResolvedActionCreateFile
FileChangeResolvedActionWriteFile
FileChangeResolvedActionUpdateFile
FileChangeResolvedActionAppendFile
FileChangeResolvedActionCreateSymlink
FileChangeResolvedActionRecreateSymlink
FileChangeResolvedActionCreateDirectoryAndSymlinks
FileChangeResolvedActionReorderSymlinks
FileChangeResolvedActionCreateDirectory
FileChangeResolvedActionRemoveDirectory
FileChangeResolvedActionTouch
FileChangeResolvedActionMountConfigFS
)
var FileChangeResolvedActionString = map[FileChangeResolvedAction]string{
FileChangeResolvedActionUnknown: "UNKNOWN",
FileChangeResolvedActionDoNothing: "DO_NOTHING",
FileChangeResolvedActionRemove: "REMOVE",
FileChangeResolvedActionCreateFile: "FILE_CREATE",
FileChangeResolvedActionWriteFile: "FILE_WRITE",
FileChangeResolvedActionUpdateFile: "FILE_UPDATE",
FileChangeResolvedActionAppendFile: "FILE_APPEND",
FileChangeResolvedActionCreateSymlink: "SYMLINK_CREATE",
FileChangeResolvedActionRecreateSymlink: "SYMLINK_RECREATE",
FileChangeResolvedActionCreateDirectoryAndSymlinks: "DIR_CREATE_AND_SYMLINKS",
FileChangeResolvedActionReorderSymlinks: "SYMLINK_REORDER",
FileChangeResolvedActionCreateDirectory: "DIR_CREATE",
FileChangeResolvedActionRemoveDirectory: "DIR_REMOVE",
FileChangeResolvedActionTouch: "TOUCH",
FileChangeResolvedActionMountConfigFS: "CONFIGFS_MOUNT",
}
type ChangeSet struct {
Changes []FileChange
}
type RequestedFileChange struct {
Component string
Key string
Path string // will be used as Key if Key is empty
ParamSymlinks []symlink
ExpectedState FileState
ExpectedContent []byte
DependsOn []string
BeforeChange []string // if the file is going to be changed, apply the change first
Description string
IgnoreErrors bool
When string // only apply the change if when meets the condition
}
type FileChange struct {
RequestedFileChange
ActualState FileState
ActualContent []byte
resolvedDeps []string
checked bool
changed ChangeState
action FileChangeResolvedAction
}
func (f *RequestedFileChange) String() string {
var s string
switch f.ExpectedState {
case FileStateDirectory:
s = fmt.Sprintf("dir: %s", f.Path)
case FileStateFile:
s = fmt.Sprintf("file: %s", f.Path)
case FileStateSymlink:
s = fmt.Sprintf("symlink: %s -> %s", f.Path, f.ExpectedContent)
case FileStateSymlinkInOrderConfigFS:
s = fmt.Sprintf("symlink_in_order_configfs: %s -> %s", f.Path, f.ExpectedContent)
case FileStateSymlinkNotInOrderConfigFS:
s = fmt.Sprintf("symlink_not_in_order_configfs: %s -> %s", f.Path, f.ExpectedContent)
case FileStateAbsent:
s = fmt.Sprintf("absent: %s", f.Path)
case FileStateFileContentMatch:
s = fmt.Sprintf("file: %s with content [%s]", f.Path, f.ExpectedContent)
case FileStateFileWrite:
s = fmt.Sprintf("write: %s with content [%s]", f.Path, f.ExpectedContent)
case FileStateMountedConfigFS:
s = fmt.Sprintf("configfs: %s", f.Path)
case FileStateTouch:
s = fmt.Sprintf("touch: %s", f.Path)
case FileStateUnknown:
s = fmt.Sprintf("unknown change for %s", f.Path)
default:
s = fmt.Sprintf("unknown expected state %d for %s", f.ExpectedState, f.Path)
}
if len(f.Description) > 0 {
s += fmt.Sprintf(" (%s)", f.Description)
}
return s
}
func (f *RequestedFileChange) IsSame(other *RequestedFileChange) bool {
return f.Path == other.Path &&
f.ExpectedState == other.ExpectedState &&
reflect.DeepEqual(f.ExpectedContent, other.ExpectedContent) &&
reflect.DeepEqual(f.DependsOn, other.DependsOn) &&
f.IgnoreErrors == other.IgnoreErrors
}
func (fc *FileChange) checkIfDirIsMountPoint() error {
// check if the file is a mount point
mounts, err := procfs.GetMounts()
if err != nil {
return fmt.Errorf("failed to get mounts")
}
for _, mount := range mounts {
if mount.MountPoint == fc.Path {
fc.ActualState = FileStateMounted
fc.ActualContent = []byte(mount.Source)
if mount.FSType == "configfs" {
fc.ActualState = FileStateMountedConfigFS
}
return nil
}
}
return nil
}
// GetActualState returns the actual state of the file at the given path.
func (fc *FileChange) getActualState() error {
l := defaultLogger.With().Str("path", fc.Path).Logger()
fi, err := os.Lstat(fc.Path)
if err != nil {
if os.IsNotExist(err) {
fc.ActualState = FileStateAbsent
} else {
l.Warn().Err(err).Msg("failed to stat file")
fc.ActualState = FileStateUnknown
}
return nil
}
// check if the file is a symlink
if fi.Mode()&os.ModeSymlink == os.ModeSymlink {
fc.ActualState = FileStateSymlink
// get the target of the symlink
target, err := os.Readlink(fc.Path)
if err != nil {
l.Warn().Err(err).Msg("failed to read symlink")
return fmt.Errorf("failed to read symlink")
}
// check if the target is a relative path
if !filepath.IsAbs(target) {
// make it absolute
target, err = filepath.Abs(filepath.Join(filepath.Dir(fc.Path), target))
if err != nil {
l.Warn().Err(err).Msg("failed to make symlink target absolute")
return fmt.Errorf("failed to make symlink target absolute")
}
}
fc.ActualContent = []byte(target)
return nil
}
if fi.IsDir() {
fc.ActualState = FileStateDirectory
switch fc.ExpectedState {
case FileStateMountedConfigFS:
err := fc.checkIfDirIsMountPoint()
if err != nil {
l.Warn().Err(err).Msg("failed to check if dir is mount point")
return err
}
case FileStateSymlinkInOrderConfigFS:
state, err := checkIfSymlinksInOrder(fc, &l)
if err != nil {
l.Warn().Err(err).Msg("failed to check if symlinks are in order")
return err
}
fc.ActualState = state
}
return nil
}
if fi.Mode()&os.ModeDevice == os.ModeDevice {
l.Info().Msg("file is a device")
return nil
}
// check if the file is a regular file
if fi.Mode().IsRegular() {
fc.ActualState = FileStateFile
// get the content of the file
content, err := os.ReadFile(fc.Path)
if err != nil {
l.Warn().Err(err).Msg("failed to read file")
return fmt.Errorf("failed to read file")
}
fc.ActualContent = content
return nil
}
l.Warn().Interface("file_info", fi.Mode()).Bool("is_dir", fi.IsDir()).Msg("unknown file type")
return fmt.Errorf("unknown file type")
}
func (fc *FileChange) ResetActionResolution() {
fc.checked = false
fc.action = FileChangeResolvedActionUnknown
fc.changed = ChangeStateUnknown
}
func (fc *FileChange) Action() FileChangeResolvedAction {
if !fc.checked {
fc.action = fc.getFileChangeResolvedAction()
fc.checked = true
}
return fc.action
}
func (fc *FileChange) getFileChangeResolvedAction() FileChangeResolvedAction {
l := defaultLogger.With().Str("path", fc.Path).Logger()
// some actions are not needed to be checked
switch fc.ExpectedState {
case FileStateFileWrite:
return FileChangeResolvedActionWriteFile
case FileStateTouch:
return FileChangeResolvedActionTouch
}
// get the actual state of the file
err := fc.getActualState()
if err != nil {
return FileChangeResolvedActionDoNothing
}
baseName := filepath.Base(fc.Path)
switch fc.ExpectedState {
case FileStateDirectory:
// if the file is already a directory, do nothing
if fc.ActualState == FileStateDirectory {
return FileChangeResolvedActionDoNothing
}
return FileChangeResolvedActionCreateDirectory
case FileStateFile:
// if the file is already a file, do nothing
if fc.ActualState == FileStateFile {
return FileChangeResolvedActionDoNothing
}
return FileChangeResolvedActionCreateFile
case FileStateFileContentMatch:
// if the file is already a file with the expected content, do nothing
if fc.ActualState == FileStateFile {
looserMatch := baseName == "inquiry_string"
if compareFileContent(fc.ActualContent, fc.ExpectedContent, looserMatch) {
return FileChangeResolvedActionDoNothing
}
// TODO: move this to somewhere else
// this is a workaround for the fact that the file is not updated if it has no content
if baseName == "file" &&
bytes.Equal(fc.ActualContent, []byte{}) &&
bytes.Equal(fc.ExpectedContent, []byte{0x0a}) {
return FileChangeResolvedActionDoNothing
}
return FileChangeResolvedActionUpdateFile
}
return FileChangeResolvedActionCreateFile
case FileStateSymlink:
// if the file is already a symlink, check if the target is the same
if fc.ActualState == FileStateSymlink {
if reflect.DeepEqual(fc.ActualContent, fc.ExpectedContent) {
return FileChangeResolvedActionDoNothing
}
return FileChangeResolvedActionRecreateSymlink
}
return FileChangeResolvedActionCreateSymlink
case FileStateSymlinkInOrderConfigFS:
// if the file is already a symlink, check if the target is the same
if fc.ActualState == FileStateSymlinkInOrderConfigFS {
return FileChangeResolvedActionDoNothing
}
return FileChangeResolvedActionReorderSymlinks
case FileStateAbsent:
if fc.ActualState == FileStateAbsent {
return FileChangeResolvedActionDoNothing
}
return FileChangeResolvedActionRemove
case FileStateMountedConfigFS:
if fc.ActualState == FileStateMountedConfigFS {
return FileChangeResolvedActionDoNothing
}
return FileChangeResolvedActionMountConfigFS
default:
l.Warn().Interface("file_change", FileStateString[fc.ExpectedState]).Msg("unknown expected state")
return FileChangeResolvedActionDoNothing
}
}
func (c *ChangeSet) AddFileChangeStruct(r RequestedFileChange) {
fc := FileChange{
RequestedFileChange: r,
}
c.Changes = append(c.Changes, fc)
}
func (c *ChangeSet) AddFileChange(component string, path string, expectedState FileState, expectedContent []byte, dependsOn []string, description string) {
c.AddFileChangeStruct(RequestedFileChange{
Component: component,
Path: path,
ExpectedState: expectedState,
ExpectedContent: expectedContent,
DependsOn: dependsOn,
Description: description,
})
}
func (c *ChangeSet) ApplyChanges() error {
r := ChangeSetResolver{
changeset: c,
g: &dag.AcyclicGraph{},
l: defaultLogger,
}
return r.Apply()
}
func (c *ChangeSet) applyChange(change *FileChange) error {
switch change.Action() {
case FileChangeResolvedActionWriteFile:
return os.WriteFile(change.Path, change.ExpectedContent, 0644)
case FileChangeResolvedActionUpdateFile:
return os.WriteFile(change.Path, change.ExpectedContent, 0644)
case FileChangeResolvedActionCreateFile:
return os.WriteFile(change.Path, change.ExpectedContent, 0644)
case FileChangeResolvedActionCreateSymlink:
return os.Symlink(string(change.ExpectedContent), change.Path)
case FileChangeResolvedActionRecreateSymlink:
if err := os.Remove(change.Path); err != nil {
return fmt.Errorf("failed to remove symlink: %w", err)
}
return os.Symlink(string(change.ExpectedContent), change.Path)
case FileChangeResolvedActionReorderSymlinks:
return recreateSymlinks(change, nil)
case FileChangeResolvedActionCreateDirectory:
return os.MkdirAll(change.Path, 0755)
case FileChangeResolvedActionRemove:
return os.Remove(change.Path)
case FileChangeResolvedActionRemoveDirectory:
return os.RemoveAll(change.Path)
case FileChangeResolvedActionTouch:
return os.Chtimes(change.Path, time.Now(), time.Now())
case FileChangeResolvedActionMountConfigFS:
return mountConfigFS(change.Path)
case FileChangeResolvedActionDoNothing:
return nil
default:
return fmt.Errorf("unknown action: %d", change.Action())
}
}
func (c *ChangeSet) Apply() error {
return c.ApplyChanges()
}