diff --git a/internal/factsengine/gatherers/cibadmin.go b/internal/factsengine/gatherers/cibadmin.go index c0a74075..921d9090 100644 --- a/internal/factsengine/gatherers/cibadmin.go +++ b/internal/factsengine/gatherers/cibadmin.go @@ -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()) } diff --git a/internal/factsengine/gatherers/gatherer.go b/internal/factsengine/gatherers/gatherer.go index 33f0876a..0b195403 100644 --- a/internal/factsengine/gatherers/gatherer.go +++ b/internal/factsengine/gatherers/gatherer.go @@ -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(), }, diff --git a/internal/factsengine/gatherers/products.go b/internal/factsengine/gatherers/products.go new file mode 100644 index 00000000..20e3e183 --- /dev/null +++ b/internal/factsengine/gatherers/products.go @@ -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 +} diff --git a/internal/factsengine/gatherers/products_test.go b/internal/factsengine/gatherers/products_test.go new file mode 100644 index 00000000..80489074 --- /dev/null +++ b/internal/factsengine/gatherers/products_test.go @@ -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(` + + + openSUSE + Leap + 15.3 + 2 + + openSUSE + Leap + 15.3 + 2 + + http://doc.opensuse.org/release-notes-openSUSE.rpm + + +`), 0777) + s.NoError(err) + + err = afero.WriteFile(fs, path.Join(testProductsPath, "otherproduct"), []byte(` + + + openSUSE + Other + 15.5 + 1 + +`), 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) + +} diff --git a/internal/factsengine/gatherers/xml.go b/internal/factsengine/gatherers/xml.go index e0cd05b1..572fb8a6 100644 --- a/internal/factsengine/gatherers/xml.go +++ b/internal/factsengine/gatherers/xml.go @@ -12,7 +12,11 @@ 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 @@ -20,7 +24,7 @@ func parseXMLToFactValueMap(xmlContent []byte, elementsToList map[string]bool) ( 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 }