From c0c924b75a6eca4f085addf4489f7b2adf65d55e Mon Sep 17 00:00:00 2001 From: brent saner Date: Thu, 20 Jun 2024 05:22:33 -0400 Subject: [PATCH] v1.3.0 ADDED: * auger, some convenience funcs around Augeas. --- TODO | 2 + auger/consts.go | 9 ++ auger/funcs.go | 63 +++++++++ auger/funcs_aug.go | 294 ++++++++++++++++++++++++++++++++++++++++ auger/funcs_augflags.go | 41 ++++++ auger/types.go | 62 +++++++++ 6 files changed, 471 insertions(+) create mode 100644 auger/consts.go create mode 100644 auger/funcs.go create mode 100644 auger/funcs_aug.go create mode 100644 auger/funcs_augflags.go create mode 100644 auger/types.go diff --git a/TODO b/TODO index 79da0ee..7130ae8 100644 --- a/TODO +++ b/TODO @@ -4,6 +4,8 @@ --- https://github.com/hlandau/passlib -- incoprporated separately; https://git.r00t2.io/r00t2/PWGen (import r00t2.io/pwgen) +- auger needs to be build-constrained to linux. + - unit tests - constants/vars for errors diff --git a/auger/consts.go b/auger/consts.go new file mode 100644 index 0000000..19f57a4 --- /dev/null +++ b/auger/consts.go @@ -0,0 +1,9 @@ +package auger + +const ( + augLensTpl string = "/augeas/load/%v" // A fmt.Sprintf string (single placeholder only) for Lens module roots. + augFsTree string = "/files" + augFsTpl string = augFsTree + "%v//%v" // A fmt.Sprintf string (first placeholder fspath, second placeholder includeDirective) for files to search for the includeDirective. + augInclTfm string = "incl" // The transformer keyword for Augeas includes. + augAppendSuffix string = "[last()+1]" +) diff --git a/auger/funcs.go b/auger/funcs.go new file mode 100644 index 0000000..beb0103 --- /dev/null +++ b/auger/funcs.go @@ -0,0 +1,63 @@ +package auger + +import ( + `io/fs` + `os` + `strings` +) + +/* + AugpathToFspath returns the filesystem path from an Augeas path. + + It is *required* and expected that the Augeas standard /files prefix be removed first; + if not, it is assumed to be part of the filesystem path. + + If a valid path cannot be determined, fsPath will be empty. +*/ +func AugpathToFspath(augPath string) (fsPath string, err error) { + + var path string + var num int + var augSplit []string = strings.Split(augPath, "/") + + num = len(augSplit) + + for i := num - 1; i >= 0; i-- { + path = strings.Join(augSplit[:i], "/") + if !fs.ValidPath(path) { + continue + } + if _, err = os.Stat(path); err != nil { + if os.IsNotExist(err) { + err = nil + continue + } else { + return + } + } + + fsPath = path + return + } + + return +} + +// dedupePaths is used to reduce new to only unique entries that do not exist in existing. +func dedupePaths(new, existing []string) (missing []string) { + + var ok bool + var m map[string]bool = make(map[string]bool) + + for _, path := range existing { + m[path] = true + } + + for _, path := range new { + if _, ok = m[path]; !ok { + missing = append(missing, path) + } + } + + return +} diff --git a/auger/funcs_aug.go b/auger/funcs_aug.go new file mode 100644 index 0000000..37f9652 --- /dev/null +++ b/auger/funcs_aug.go @@ -0,0 +1,294 @@ +package auger + +import ( + `bufio` + `errors` + `fmt` + `io` + `os` + `path/filepath` + `strings` + + `github.com/davecgh/go-spew/spew` + `github.com/google/shlex` + `honnef.co/go/augeas` + `r00t2.io/sysutils/paths` +) + +// Close cleanly closes the underlying Augeas connection. +func (a *Aug) Close() { + + a.aug.Close() + +} + +// Interact provides an interactive shell-like interface for debugging purposes to explore the loaded Augeas tree. +func (a *Aug) Interact() (err error) { + + var input string + var lexed []string + var cmd string + var arg string + var val string + var augVal string + var augVals []string + var buf *bufio.Reader = bufio.NewReader(os.Stdin) + + fmt.Fprint(os.Stderr, "INTERACTIVE MODE\nCmds: get, getall, match, set, quit\n") +breakCmd: + for { + cmd, arg, val = "", "", "" + + fmt.Fprint(os.Stderr, "> ") + if input, err = buf.ReadString('\n'); err != nil { + if errors.Is(err, io.EOF) { + err = nil + break breakCmd + } + return + } + if strings.TrimSpace(input) == "" { + continue + } + if lexed, err = shlex.Split(input); err != nil { + return + } + if lexed == nil || len(lexed) == 0 || len(lexed) > 3 { + fmt.Fprintf(os.Stderr, "Bad command: %#v\n", lexed) + continue + } + cmd = lexed[0] + switch len(lexed) { + case 2: + arg = lexed[1] + case 3: + arg = lexed[1] + val = lexed[2] + } + switch cmd { + case "get": + if strings.TrimSpace(arg) == "" { + fmt.Fprintln(os.Stderr, "Missing argument") + continue + } + if augVal, err = a.aug.Get(arg); err != nil { + fmt.Fprintf(os.Stderr, "!! ERROR !!\n%v\n", spew.Sdump(err)) + err = nil + continue + } + fmt.Fprintln(os.Stderr, augVal) + case "getall": + if strings.TrimSpace(arg) == "" { + fmt.Fprintln(os.Stderr, "Missing argument") + continue + } + if augVals, err = a.aug.GetAll(arg); err != nil { + fmt.Fprintf(os.Stderr, "!! ERROR !!\n%v\n", spew.Sdump(err)) + err = nil + continue + } + fmt.Fprintln(os.Stderr, strings.Join(augVals, "\n")) + case "match": + if strings.TrimSpace(arg) == "" { + fmt.Fprintln(os.Stderr, "Missing argument") + continue + } + if augVals, err = a.aug.Match(arg); err != nil { + fmt.Fprintf(os.Stderr, "!! ERROR !!\n%v\n", spew.Sdump(err)) + err = nil + continue + } + fmt.Fprintln(os.Stderr, strings.Join(augVals, "\n")) + case "set": + if strings.TrimSpace(arg) == "" { + fmt.Fprintln(os.Stderr, "Missing argument") + continue + } + if strings.TrimSpace(val) == "" { + fmt.Fprintln(os.Stderr, "Missing value") + continue + } + if err = a.aug.Set(arg, val); err != nil { + fmt.Fprintf(os.Stderr, "!! ERROR !!\n%v\n", spew.Sdump(err)) + err = nil + continue + } + fmt.Fprintln(os.Stderr, "Success!") + case "quit": + break breakCmd + default: + fmt.Fprintf(os.Stderr, "Unknown command: %v\n", cmd) + continue + } + } + + return +} + +/* + RecursiveInclude parses the configuration files belonging to Augeas lens name augLens, + searching for all occurrences of includeDirective, loading those files (if they exist), + and continuing so forth recursively, loading them into the Augeas file tree. + + If any relative paths are found, they will be assumed to be relative to fsRoot ("/" if empty). + For e.g. Nginx, you almost absolutely want to set this to "/etc/nginx", but you really should + use absolute paths for every include in your configs if supported by the application; it will + lead to much less guesswork and much more accurate recursing/walking. + + Some lens recursively load depending on their respective include directive(s) automatically; + some (such as the Nginx lens) do not. + + For example for Nginx, augLens should be "Nginx". RecursiveInclude will then iterate over + /augeas/load/Nginx/incl (/augeas/load//incl), parsing each file for includeDirective + (the "include" keyword, in Nginx's case), check if it is already loaded in /augeas/load//incl, + adding it and reloading if not, and then scanning *that* file for includeDirective, etc. + + An error will be returned if augLens is a nonexistent or not-loaded Augeas lens module. + + Depending on how many files there are and whether globs vs. explicit filepaths are included, this may take a while. +*/ +func (a *Aug) RecursiveInclude(augLens, includeDirective, fsRoot string) (err error) { + + if err = a.addIncl(includeDirective, augLens, fsRoot, nil); err != nil { + return + } + + return +} + +/* + addIncl is used by RecursiveInclude. + + includeDirective, augLens, and fsRoot have the same meaning as in RecursiveInclude. + + newInclPaths are new filesystem paths/Augeas-compatible glob patterns to load into the filetree and recurse into. + They may be nil, especially if the first run. +*/ +func (a *Aug) addIncl(includeDirective, augLens string, fsRoot string, newInclPaths []string) (err error) { + + var matches []string // Passed around set of Augeas matches. + var includes []string // Filepath(s)/glob(s) from fetching includeDirective in lensInclPath. These are internal to the application but are recursed. + var lensInclPath string // The path of the included paths in the tree. These are internal to Augeas, not the application. + var appendPath string // The path for new Augeas includes. + var match []string // A placeholder for iterating when populating includes. + var fpath string // A placeholder for finding the path of a conf file that contains an includeDirective. + var lensPath string = fmt.Sprintf(augLensTpl, augLens) // The path of the lens (augLens) itself. + var augErr *augeas.Error = new(augeas.Error) // We use this to skip "nonexistent" lens. + + if fsRoot == "" { + fsRoot = "/" + } + if err = paths.RealPath(&fsRoot); err != nil { + return + } + + for strings.HasSuffix(lensPath, "/") { + lensPath = lensPath[:len(lensPath)-1] + } + if !strings.HasSuffix(lensPath, "/"+augInclTfm) { + lensPath = strings.TrimSuffix(lensPath, "/"+augInclTfm) + } + lensInclPath = fmt.Sprintf("%v/%v", lensPath, augInclTfm) + appendPath = fmt.Sprintf("%v/%v", lensInclPath, augAppendSuffix) + + // First canonize paths. + if newInclPaths != nil && len(newInclPaths) > 0 { + // Existing includes. We don't return on an empty lensInclPath because + if matches, err = a.aug.Match(lensInclPath); err != nil { + if errors.As(err, augErr) && augErr.Code == augeas.NoMatch { + err = nil + } else { + return + } + } else { + for idx, m := range matches { + if matches[idx], err = a.aug.Get(m); err != nil { + return + } + } + } + + // Normalize new include(s). + for idx, i := range newInclPaths { + if !filepath.IsAbs(i) { + newInclPaths[idx] = filepath.Join(fsRoot, i) + } + if err = paths.RealPath(&newInclPaths[idx]); err != nil { + return + } + } + + // We don't want to bother adding multiple incl's for the same path(s); it can negatively affect Augeas loads. + newInclPaths = dedupePaths(newInclPaths, matches) + + // Add the new path(s) as Augeas include entries. + if newInclPaths != nil { + for _, fsPath := range newInclPaths { + if err = a.aug.Set(appendPath, fsPath); err != nil { + return + } + } + /* + And then load the new files into the tree. + This is done at the end as it takes WAYYY less time to just reload the tree + as a whole once you have more than, say, 30 files added at a time. + */ + if err = a.aug.Load(); err != nil { + return + } + } + } + + // We now fetch all values (filepath/globs) that match the includeDirective and recurse with those as new include files. + matches = nil + if includes, err = a.aug.GetAll(lensInclPath); err != nil { + return + } + for _, fsPath := range includes { + // This gets the Augeas filetree path, NOT the FS path... + if match, err = a.aug.Match(fmt.Sprintf(augFsTpl, fsPath, includeDirective)); err != nil { + return + } + if match == nil || len(match) == 0 { + continue + } + /* + For each directive match, we need to: + + 1.) normalize to an FS *file* path (strip augFsTree from the beginning + 2.) walk backwards (via AugpathToFspath) until we find an actual file + 3.) get the *dirname* of that file + 4.) join the value (included file) to #3 + IF + fsRoot == "/" + + This very obviously breaks for applications with arbitrary roots (like Nginx including relative to /etc/nginx). + That's why we warn about it in Aug.RecursiveInclude. Caveat emptor. + */ + for idx, ftreePath := range match { + if fpath, err = a.aug.Get(ftreePath); err != nil { + return + } + if fsRoot == "/" { + if ftreePath, err = AugpathToFspath( + strings.TrimSuffix(ftreePath, augFsTree), + ); err != nil { + return + } + if ftreePath != "" { + fpath = filepath.Join(filepath.Dir(ftreePath), fpath) + } + } + match[idx] = fpath + } + matches = append(matches, match...) + } + + if matches != nil && len(matches) != 0 { + if err = a.addIncl(includeDirective, augLens, fsRoot, matches); err != nil { + return + } + } + + return +} diff --git a/auger/funcs_augflags.go b/auger/funcs_augflags.go new file mode 100644 index 0000000..19e74ce --- /dev/null +++ b/auger/funcs_augflags.go @@ -0,0 +1,41 @@ +package auger + +import ( + `honnef.co/go/augeas` +) + +// Eval returns an evaluated set of flags. +func (a *AugFlags) Eval() (augFlags augeas.Flag) { + + augFlags = augeas.None + + if a.Backup != nil && *a.Backup { + augFlags |= augeas.SaveBackup + } + if a.NewFile != nil && *a.NewFile { + augFlags |= augeas.SaveNewFile + } + if a.TypeCheck != nil && *a.TypeCheck { + augFlags |= augeas.TypeCheck + } + if a.NoDfltModLoad != nil && *a.NoDfltModLoad { + augFlags |= augeas.NoModlAutoload + } + if a.DryRun != nil && *a.DryRun { + augFlags |= augeas.SaveNoop + } + if a.NoTree != nil && *a.NoTree { + augFlags |= augeas.NoLoad + } + if a.NoAutoModLoad != nil && *a.NoAutoModLoad { + augFlags |= augeas.NoModlAutoload + } + if a.EnableSpan != nil && *a.EnableSpan { + augFlags |= augeas.EnableSpan + } + if a.NoErrClose != nil && *a.NoErrClose { + augFlags |= augeas.NoErrClose + } + + return +} diff --git a/auger/types.go b/auger/types.go new file mode 100644 index 0000000..f0e942d --- /dev/null +++ b/auger/types.go @@ -0,0 +1,62 @@ +package auger + +import ( + `honnef.co/go/augeas` +) + +// Aug is a wrapper around (honnef.co/go/)augeas.Augeas. Remember to call Aug.Close(). +type Aug struct { + aug augeas.Augeas +} + +// AugFlags contains flags to pass to the Augeas instance. +type AugFlags struct { + /* + Backup, if true, will enable Augeas backup mode (original files are saved with a .augsave suffix). + const: augeas.SaveBackup + */ + Backup *bool `toml:"Backup,omitempty"` + /* + NewFile, if true, will create new files (.augnew suffix) instead of overwriting the original file. + const: augeas.SaveNewFile + */ + NewFile *bool `toml:"NewFile,omitempty"` + /* + TypeCheck, if true, will perform a Lens typecheck. + HIGHLY UNRECOMMENDED; WILL INDUCE A HUGE FRONTLOAD. + const: augeas.TypeCheck + */ + TypeCheck *bool `toml:"TypeCheck,omitempty"` + /* + NoDfltModLoad, if true, will suppress loading the built-in/default modules. + Highly unrecommended, as we do not have a current way in the config to define load paths (yet). + const: augeas.NoStdinc + */ + NoDfltModLoad *bool `toml:"NoDfltModLoad,omitempty"` + /* + DryRun, if true, will make all saves NO-OPs. + const: augeas.SaveNoop + */ + DryRun *bool `toml:"DryRun,omitempty"` + /* + NoTree, if true, will not load the filetree automatically. Doesn't really affect this program. + const: augeas.NoLoad + */ + NoTree *bool `toml:"NoTree,omitempty"` + /* + NoAutoModLoad, if true, will supress automatically loading modules. + const: augeas.NoModlAutoload + */ + NoAutoModLoad *bool `toml:"NoAutoModLoad,omitempty"` + /* + EnableSpan, if true, will track span in input nodes (location information, essentially). + See https://augeas.net/docs/api.html#getting-the-span-of-a-node-related-to-a-file + const: augeas.EnableSpan + */ + EnableSpan *bool `toml:"EnableSpan,omitempty"` + /* + NoErrClose, if true, will suppress automatically closing on error. + const: augeas.NoErrClose + */ + NoErrClose *bool `toml:"NoErrClose,omitempty"` +}