ADDED:
* auger, some convenience funcs around Augeas.
This commit is contained in:
brent saner 2024-06-20 05:22:33 -04:00
parent b64c318a4a
commit c0c924b75a
Signed by: bts
GPG Key ID: 8C004C2F93481F6B
6 changed files with 471 additions and 0 deletions

2
TODO
View File

@ -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

9
auger/consts.go Normal file
View File

@ -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]"
)

63
auger/funcs.go Normal file
View File

@ -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
}

294
auger/funcs_aug.go Normal file
View File

@ -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/<augLens>/incl), parsing each file for includeDirective
(the "include" keyword, in Nginx's case), check if it is already loaded in /augeas/load/<augLens>/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
}

41
auger/funcs_augflags.go Normal file
View File

@ -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
}

62
auger/types.go Normal file
View File

@ -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"`
}