mirror of https://github.com/jetkvm/kvm.git
433 lines
13 KiB
Go
433 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_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()
|
|
}
|