Skip to content

Commit

Permalink
Products gatherer (#285)
Browse files Browse the repository at this point in the history
* Accept fact value options in xml reader

* Implement products gatherer

* Add products gatherer to the standard list

* Cleanup tests

* Update function name to be more specific
  • Loading branch information
arbulu89 authored Oct 31, 2023
1 parent c4ab8e2 commit a54dd6b
Show file tree
Hide file tree
Showing 5 changed files with 287 additions and 3 deletions.
2 changes: 1 addition & 1 deletion internal/factsengine/gatherers/cibadmin.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ func (g *CibAdminGatherer) Gather(factsRequests []entities.FactRequest) ([]entit
"nvpair": true, "op": true, "rsc_location": true, "rsc_order": true,
"rsc_colocation": true, "cluster_property_set": true, "meta_attributes": true}

factValueMap, err := parseXMLToFactValueMap(cibadmin, elementsToList)
factValueMap, err := parseXMLToFactValueMap(cibadmin, elementsToList, entities.WithStringConversion())
if err != nil {
return nil, CibAdminDecodingError.Wrap(err.Error())
}
Expand Down
3 changes: 3 additions & 0 deletions internal/factsengine/gatherers/gatherer.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ func StandardGatherers() FactGatherersTree {
PasswdGathererName: map[string]FactGatherer{
"v1": NewDefaultPasswdGatherer(),
},
ProductsGathererName: map[string]FactGatherer{
"v1": NewDefaultProductsGatherer(),
},
SapControlGathererName: map[string]FactGatherer{
"v1": NewDefaultSapControlGatherer(),
},
Expand Down
110 changes: 110 additions & 0 deletions internal/factsengine/gatherers/products.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package gatherers

import (
"path"

"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
"github.com/spf13/afero"
"github.com/trento-project/agent/pkg/factsengine/entities"
)

const (
ProductsGathererName = "products"
productsDefaultPath = "/etc/products.d/"
)

// nolint:gochecknoglobals
var (
ProductsFolderMissingError = entities.FactGatheringError{
Type: "products-folder-missing-error",
Message: "products folder does not exist",
}

ProductsFolderReadingError = entities.FactGatheringError{
Type: "products-folder-reading-error",
Message: "error reading the products folder",
}

ProductsFileReadingError = entities.FactGatheringError{
Type: "products-file-reading-error",
Message: "error reading the products file",
}

productsXMLelementsToList = map[string]bool{
"distrotarget": true,
"repository": true,
"language": true,
"url": true,
"productdependency": true,
}
)

type ProductsGatherer struct {
fs afero.Fs
productsPath string
}

func NewProductsGatherer(fs afero.Fs, productsPath string) *ProductsGatherer {
return &ProductsGatherer{
fs: fs,
productsPath: productsPath,
}
}

func NewDefaultProductsGatherer() *ProductsGatherer {
return &ProductsGatherer{fs: afero.NewOsFs(), productsPath: productsDefaultPath}
}

func (g *ProductsGatherer) Gather(factsRequests []entities.FactRequest) ([]entities.Fact, error) {
facts := []entities.Fact{}
log.Infof("Starting %s facts gathering process", ProductsGathererName)

if exists, _ := afero.DirExists(g.fs, g.productsPath); !exists {
gatheringError := ProductsFolderMissingError.Wrap(g.productsPath)
log.Error(gatheringError.Error())
return nil, gatheringError
}

productFiles, err := afero.ReadDir(g.fs, g.productsPath)
if err != nil {
gatheringError := ProductsFolderReadingError.Wrap(g.productsPath).Wrap(err.Error())
log.Error(gatheringError.Error())
return nil, gatheringError
}

productsFactValueMap := make(map[string]entities.FactValue)
for _, productFile := range productFiles {
productFileName := productFile.Name()
product, err := parseProductFile(g.fs, path.Join(g.productsPath, productFileName))
if err != nil {
gatheringError := ProductsFileReadingError.Wrap(productFileName).Wrap(err.Error())
log.Error(gatheringError.Error())
return nil, gatheringError
}

productsFactValueMap[productFileName] = product
}

for _, requestedFact := range factsRequests {
facts = append(facts, entities.NewFactGatheredWithRequest(
requestedFact, &entities.FactValueMap{Value: productsFactValueMap}))
}

log.Infof("Requested %s facts gathered", ProductsGathererName)
return facts, nil
}

func parseProductFile(fs afero.Fs, productFilePath string) (entities.FactValue, error) {
productFile, err := afero.ReadFile(fs, productFilePath)
if err != nil {
return nil, errors.Wrap(err, "could not open product file")
}

factValueMap, err := parseXMLToFactValueMap(productFile, productsXMLelementsToList)
if err != nil {
return nil, errors.Wrap(err, "could not parse product file")
}

return factValueMap, nil
}
167 changes: 167 additions & 0 deletions internal/factsengine/gatherers/products_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
package gatherers_test

import (
"path"
"testing"

"github.com/spf13/afero"
"github.com/stretchr/testify/suite"
"github.com/trento-project/agent/internal/factsengine/gatherers"
"github.com/trento-project/agent/pkg/factsengine/entities"
)

const testProductsPath string = "/etc/products.d/"

type ProductsGathererSuite struct {
suite.Suite
}

func TestProductsGathererSuite(t *testing.T) {
suite.Run(t, new(ProductsGathererSuite))
}

func (s *ProductsGathererSuite) TestProductsGathererFolderMissingError() {
fs := afero.NewMemMapFs()

fr := []entities.FactRequest{
{
Name: "missing_folder",
Gatherer: "products@v1",
CheckID: "check1",
},
}

gatherer := gatherers.NewProductsGatherer(fs, testProductsPath)

results, err := gatherer.Gather(fr)
s.Nil(results)
s.EqualError(err, "fact gathering error: products-folder-missing-error - "+
"products folder does not exist: /etc/products.d/")
}

func (s *ProductsGathererSuite) TestProductsGathererReadingError() {
fs := afero.NewMemMapFs()

err := afero.WriteFile(fs, path.Join(testProductsPath, "baseproduct"), []byte(`
<?xml version="1.0" encoding="UTF-8"?>
<product schemeversion="0">
<vendor>openSUSE</vendor>
<name>Leap</name>
<version>15.3</version>
<release>2</releas
`), 0777)

s.NoError(err)

fr := []entities.FactRequest{
{
Name: "invalid_xml",
Gatherer: "products@v1",
CheckID: "check1",
},
}

gatherer := gatherers.NewProductsGatherer(fs, testProductsPath)

results, err := gatherer.Gather(fr)
s.Nil(results)
s.EqualError(err, "fact gathering error: products-file-reading-error - "+
"error reading the products file: baseproduct: could not parse product file: "+
"xml.Decoder.Token() - XML syntax error on line 8: unexpected EOF")
}

func (s *ProductsGathererSuite) TestProductsGathererSuccess() {
fs := afero.NewMemMapFs()

err := afero.WriteFile(fs, path.Join(testProductsPath, "baseproduct"), []byte(`
<?xml version="1.0" encoding="UTF-8"?>
<product schemeversion="0">
<vendor>openSUSE</vendor>
<name>Leap</name>
<version>15.3</version>
<release>2</release>
<urls>
<url name="releasenotes">http://doc.opensuse.org/release-notes-openSUSE.rpm</url>
</urls>
</product>
`), 0777)
s.NoError(err)

err = afero.WriteFile(fs, path.Join(testProductsPath, "otherproduct"), []byte(`
<?xml version="1.0" encoding="UTF-8"?>
<product schemeversion="0">
<vendor>openSUSE</vendor>
<name>Other</name>
<version>15.5</version>
<release>1</release>
</product>
`), 0777)
s.NoError(err)

fr := []entities.FactRequest{
{
Name: "products",
Gatherer: "products@v1",
CheckID: "check1",
},
}

gatherer := gatherers.NewProductsGatherer(fs, testProductsPath)

expectedFacts := []entities.Fact{
{
Name: "products",
CheckID: "check1",
Value: &entities.FactValueMap{
Value: map[string]entities.FactValue{
"baseproduct": &entities.FactValueMap{
Value: map[string]entities.FactValue{
"product": &entities.FactValueMap{
Value: map[string]entities.FactValue{
"schemeversion": &entities.FactValueInt{Value: 0},
"vendor": &entities.FactValueString{Value: "openSUSE"},
"name": &entities.FactValueString{Value: "Leap"},
"version": &entities.FactValueString{Value: "15.3"},
"release": &entities.FactValueString{Value: "2"},
"urls": &entities.FactValueMap{
Value: map[string]entities.FactValue{
"url": &entities.FactValueList{
Value: []entities.FactValue{
&entities.FactValueMap{
Value: map[string]entities.FactValue{
"name": &entities.FactValueString{Value: "releasenotes"},
"#text": &entities.FactValueString{Value: "http://doc.opensuse.org/release-notes-openSUSE.rpm"},
},
},
},
},
},
},
},
},
},
},
"otherproduct": &entities.FactValueMap{
Value: map[string]entities.FactValue{
"product": &entities.FactValueMap{
Value: map[string]entities.FactValue{
"schemeversion": &entities.FactValueInt{Value: 0},
"vendor": &entities.FactValueString{Value: "openSUSE"},
"name": &entities.FactValueString{Value: "Other"},
"version": &entities.FactValueString{Value: "15.5"},
"release": &entities.FactValueString{Value: "1"},
},
},
},
},
},
},
Error: nil,
},
}

results, err := gatherer.Gather(fr)
s.NoError(err)
s.EqualValues(expectedFacts, results)

}
8 changes: 6 additions & 2 deletions internal/factsengine/gatherers/xml.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,19 @@ func init() {
mxj.PrependAttrWithHyphen(false)
}

func parseXMLToFactValueMap(xmlContent []byte, elementsToList map[string]bool) (*entities.FactValueMap, error) {
func parseXMLToFactValueMap(
xmlContent []byte,
elementsToList map[string]bool,
factValueOpts ...entities.FactValueOption) (*entities.FactValueMap, error) {

mv, err := mxj.NewMapXml(xmlContent)
if err != nil {
return nil, err
}

mapValue := map[string]interface{}(mv)
updatedMap := convertListElements(mapValue, elementsToList)
factValue, err := entities.NewFactValue(updatedMap, entities.WithStringConversion())
factValue, err := entities.NewFactValue(updatedMap, factValueOpts...)
if err != nil {
return nil, err
}
Expand Down

0 comments on commit a54dd6b

Please sign in to comment.