Skip to content

Commit

Permalink
Implement /etc/hosts file gatherer (#78)
Browse files Browse the repository at this point in the history
* 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
rtorrero authored Nov 15, 2022
1 parent f8bc0ad commit 4d97f04
Show file tree
Hide file tree
Showing 4 changed files with 282 additions and 2 deletions.
7 changes: 5 additions & 2 deletions internal/factsengine/gatherers/gatherer.go
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(),
}
}
150 changes: 150 additions & 0 deletions internal/factsengine/gatherers/hostsfile.go
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
}
118 changes: 118 additions & 0 deletions internal/factsengine/gatherers/hostsfile_test.go
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)
}
9 changes: 9 additions & 0 deletions test/fixtures/gatherers/hosts.basic
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

0 comments on commit 4d97f04

Please sign in to comment.