mirror of
https://github.com/luckfox-eng29/kvm.git
synced 2026-01-18 03:28:19 +01:00
chore(usbgadget): update usbgadget config only when needed (#474)
This commit is contained in:
432
internal/usbgadget/changeset.go
Normal file
432
internal/usbgadget/changeset.go
Normal file
@@ -0,0 +1,432 @@
|
||||
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_MOUNT",
|
||||
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)
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
Reference in New Issue
Block a user