-
Notifications
You must be signed in to change notification settings - Fork 11
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implement /etc/hosts file gatherer (#78)
* Add /etc/hosts file gatherer * Add tests for the /etc/hosts file gatherer * Adapt to use FactValue * Allow gathered facts to contain errors * Split long line so linter doesn't hate me anymore * Address comments
- Loading branch information
Showing
4 changed files
with
282 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,13 +1,16 @@ | ||
package gatherers | ||
|
||
import "github.com/trento-project/agent/internal/factsengine/entities" | ||
import ( | ||
"github.com/trento-project/agent/internal/factsengine/entities" | ||
) | ||
|
||
type FactGatherer interface { | ||
Gather(factsRequests []entities.FactRequest) ([]entities.Fact, error) | ||
} | ||
|
||
func StandardGatherers() map[string]FactGatherer { | ||
return map[string]FactGatherer{ | ||
CorosyncFactKey: NewDefaultCorosyncConfGatherer(), | ||
CorosyncFactKey: NewDefaultCorosyncConfGatherer(), | ||
HostsFileFactKey: NewDefaultHostsFileGatherer(), | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,150 @@ | ||
package gatherers | ||
|
||
import ( | ||
"bufio" | ||
"fmt" | ||
"os" | ||
"regexp" | ||
"strings" | ||
|
||
log "github.com/sirupsen/logrus" | ||
"github.com/trento-project/agent/internal/factsengine/entities" | ||
) | ||
|
||
const ( | ||
HostsFileFactKey = "hosts" | ||
HostsFilePath = "/etc/hosts" | ||
ipMatchGroup = "ip" | ||
hostnamesMatchGroup = "hostnames" | ||
parsingRegexp = `(?m)(?P<` + ipMatchGroup + `>\S+)\s+(?P<` + hostnamesMatchGroup + `>.+)` | ||
) | ||
|
||
var ( | ||
hostsEntryCompiled = regexp.MustCompile(parsingRegexp) | ||
) | ||
|
||
// nolint:gochecknoglobals | ||
var ( | ||
HostsFileError = entities.FactGatheringError{ | ||
Type: "hosts-file-error", | ||
Message: "error reading /etc/hosts file", | ||
} | ||
|
||
HostsFileDecodingError = entities.FactGatheringError{ | ||
Type: "hosts-file-decoding-error", | ||
Message: "error decoding /etc/hosts file", | ||
} | ||
|
||
HostsEntryNotFoundError = entities.FactGatheringError{ | ||
Type: "hosts-file-value-not-found", | ||
Message: "requested field value not found in /etc/hosts file", | ||
} | ||
) | ||
|
||
type HostsFileGatherer struct { | ||
hostsFilePath string | ||
} | ||
|
||
func NewDefaultHostsFileGatherer() *HostsFileGatherer { | ||
return NewHostsFileGatherer(HostsFilePath) | ||
} | ||
|
||
func NewHostsFileGatherer(hostsFile string) *HostsFileGatherer { | ||
return &HostsFileGatherer{hostsFilePath: hostsFile} | ||
} | ||
|
||
func (s *HostsFileGatherer) Gather(factsRequests []entities.FactRequest) ([]entities.Fact, error) { | ||
facts := []entities.Fact{} | ||
log.Infof("Starting /etc/hosts file facts gathering process") | ||
|
||
hostsFile, err := readHostsFileByLines(s.hostsFilePath) | ||
if err != nil { | ||
return nil, HostsFileError.Wrap(err.Error()) | ||
} | ||
|
||
hostsFileMap, err := hostsFileToMap(hostsFile) | ||
if err != nil { | ||
return nil, HostsFileDecodingError.Wrap(err.Error()) | ||
} | ||
|
||
for _, factReq := range factsRequests { | ||
var fact entities.Fact | ||
|
||
if ip, found := hostsFileMap.Value[factReq.Argument]; found { | ||
fact = entities.NewFactGatheredWithRequest(factReq, ip) | ||
} else { | ||
gatheringError := HostsEntryNotFoundError.Wrap(factReq.Argument) | ||
log.Error(gatheringError) | ||
fact = entities.NewFactGatheredWithError(factReq, gatheringError) | ||
} | ||
facts = append(facts, fact) | ||
} | ||
|
||
log.Infof("Requested /etc/hosts file facts gathered") | ||
return facts, nil | ||
} | ||
|
||
func readHostsFileByLines(filePath string) ([]string, error) { | ||
hostsFile, err := os.Open(filePath) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
defer func() { | ||
err := hostsFile.Close() | ||
if err != nil { | ||
log.Error(err) | ||
} | ||
}() | ||
|
||
fileScanner := bufio.NewScanner(hostsFile) | ||
fileScanner.Split(bufio.ScanLines) | ||
var fileLines []string | ||
|
||
for fileScanner.Scan() { | ||
scannedLine := fileScanner.Text() | ||
if strings.HasPrefix(scannedLine, "#") || scannedLine == "" { | ||
continue | ||
} | ||
fileLines = append(fileLines, scannedLine) | ||
} | ||
|
||
return fileLines, nil | ||
} | ||
|
||
func hostsFileToMap(lines []string) (*entities.FactValueMap, error) { | ||
var hostsFileMap = make(map[string]entities.FactValue) | ||
|
||
var paramsMap = make(map[string]string) | ||
|
||
for _, line := range lines { | ||
match := hostsEntryCompiled.FindStringSubmatch(line) | ||
|
||
if match == nil { | ||
return nil, fmt.Errorf("invalid hosts file structure") | ||
} | ||
for i, name := range hostsEntryCompiled.SubexpNames() { | ||
if i > 0 && i <= len(match) { | ||
paramsMap[name] = match[i] | ||
} | ||
} | ||
hostnames := strings.Fields(paramsMap["hostnames"]) | ||
|
||
for _, hostname := range hostnames { | ||
if ip, found := hostsFileMap[hostname]; found { | ||
if ipsByHostname, ok := ip.(*entities.FactValueList); ok { | ||
ipsByHostname.Value = append(ipsByHostname.Value, &entities.FactValueString{Value: paramsMap["ip"]}) | ||
} else { | ||
return nil, fmt.Errorf("casting error while mapping ips to hosts") | ||
} | ||
|
||
} else { | ||
hostsFileMap[hostname] = &entities.FactValueList{Value: []entities.FactValue{ | ||
&entities.FactValueString{Value: paramsMap["ip"]}, | ||
}} | ||
} | ||
} | ||
} | ||
|
||
return &entities.FactValueMap{Value: hostsFileMap}, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,118 @@ | ||
package gatherers | ||
|
||
import ( | ||
"testing" | ||
|
||
"github.com/stretchr/testify/suite" | ||
"github.com/trento-project/agent/internal/factsengine/entities" | ||
"github.com/trento-project/agent/test/helpers" | ||
) | ||
|
||
type HostsFileTestSuite struct { | ||
suite.Suite | ||
} | ||
|
||
func TestHostsFileTestSuite(t *testing.T) { | ||
suite.Run(t, new(HostsFileTestSuite)) | ||
} | ||
|
||
func (suite *HostsFileTestSuite) TestHostsFileBasic() { | ||
c := NewHostsFileGatherer(helpers.GetFixturePath("gatherers/hosts.basic")) | ||
|
||
factRequests := []entities.FactRequest{ | ||
{ | ||
Name: "hosts_localhost", | ||
Gatherer: "hosts", | ||
Argument: "localhost", | ||
CheckID: "check1", | ||
}, | ||
{ | ||
Name: "hosts_somehost", | ||
Gatherer: "hosts", | ||
Argument: "somehost", | ||
CheckID: "check2", | ||
}, | ||
{ | ||
Name: "hosts_ip6-localhost", | ||
Gatherer: "hosts", | ||
Argument: "ip6-localhost", | ||
CheckID: "check3", | ||
}, | ||
} | ||
|
||
factResults, err := c.Gather(factRequests) | ||
|
||
expectedResults := []entities.Fact{ | ||
{ | ||
Name: "hosts_localhost", | ||
Value: &entities.FactValueList{Value: []entities.FactValue{ | ||
&entities.FactValueString{Value: "127.0.0.1"}, | ||
&entities.FactValueString{Value: "::1"}, | ||
}}, | ||
CheckID: "check1", | ||
}, | ||
{ | ||
Name: "hosts_somehost", | ||
Value: &entities.FactValueList{Value: []entities.FactValue{ | ||
&entities.FactValueString{Value: "127.0.1.1"}, | ||
}}, | ||
CheckID: "check2", | ||
}, | ||
{ | ||
Name: "hosts_ip6-localhost", | ||
Value: &entities.FactValueList{Value: []entities.FactValue{ | ||
&entities.FactValueString{Value: "::1"}, | ||
}}, | ||
CheckID: "check3", | ||
}, | ||
} | ||
|
||
suite.NoError(err) | ||
suite.ElementsMatch(expectedResults, factResults) | ||
} | ||
|
||
func (suite *HostsFileTestSuite) TestHostsFileNotExists() { | ||
c := NewHostsFileGatherer("non_existing_file") | ||
|
||
factRequests := []entities.FactRequest{ | ||
{ | ||
Name: "hosts_somehost", | ||
Gatherer: "hosts", | ||
Argument: "somehost", | ||
}, | ||
} | ||
|
||
_, err := c.Gather(factRequests) | ||
|
||
suite.EqualError(err, "fact gathering error: hosts-file-error - error reading /etc/hosts file: "+ | ||
"open non_existing_file: no such file or directory") | ||
} | ||
|
||
func (suite *HostsFileTestSuite) TestHostsFileIgnoresCommentedHosts() { | ||
|
||
c := NewHostsFileGatherer(helpers.GetFixturePath("gatherers/hosts.basic")) | ||
|
||
factRequests := []entities.FactRequest{ | ||
{ | ||
Name: "hosts_commented-host", | ||
Gatherer: "hosts", | ||
Argument: "commented-host", | ||
}, | ||
} | ||
|
||
factResults, err := c.Gather(factRequests) | ||
|
||
expectedResults := []entities.Fact{ | ||
{ | ||
Name: "hosts_commented-host", | ||
Value: nil, | ||
Error: &entities.FactGatheringError{ | ||
Message: "requested field value not found in /etc/hosts file: commented-host", | ||
Type: "hosts-file-value-not-found", | ||
}, | ||
}, | ||
} | ||
|
||
suite.NoError(err) | ||
suite.ElementsMatch(expectedResults, factResults) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
# Host addresses | ||
127.0.0.1 localhost | ||
127.0.1.1 somehost | ||
52.84.66.74 suse.com | ||
|
||
#127.0.0.1 commented-host | ||
::1 localhost ip6-localhost ip6-loopback | ||
ff02::1 ip6-allnodes | ||
ff02::2 ip6-allrouters |