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