295 lines
8.8 KiB
Go
295 lines
8.8 KiB
Go
|
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
|
||
|
}
|