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() }