diff --git a/README.md b/README.md
index eb01eb50e..696c1a170 100644
--- a/README.md
+++ b/README.md
@@ -193,16 +193,17 @@ Notice that a new entry must exists for every node.
- `config-dir`: Consul agent configuration files directory. It must be the same used by the consul agent. The `trento` agent creates a new folder with the node name where the trento meta-data configuration file is stored (e.g. `consul.d/node1/trento-config.json`).
- `consul-template`: Template used to populate the trento meta-data configuration file (by default [meta-data file][./examples/trento-config.json] is used).
-### Filtering the nodes in the wep app
+### Grouping and filtering the nodes in the wep app
-The web app provides the option to filter the systems using the previously commented reserved tags. To achieve this, the tags must be stored in the KV storage.
+The app provides the option to see the environment composed by the nodes and filter the systems using the previously commented reserved tags. To achieve this, the tags must be stored in the KV storage.
Use the next path:
-- `trento/filters/sap-environments`
-- `trento/filters/sap-landscapes`
-- `trento/filters/sap-systems`
+- `trento/environments/$yourenv/`
+- `trento/environments/$yourenv/landscapes/$yourland/`
+- `trento/environments/$yourenv/landscapes/$yourland/sapsystems/$yoursapsy`
-Each of them must have a json list format. As example: `["land1", "land2"]`.
-These entries will be available in the filters on the `/environments` page.
+Keep in mind that the created environments, landscapes and sap systems are directories themselves, and there can be multiple of them.
+The possibility to have multiple landscapes with the same name in different environments (and the same for SAP systems) is possible.
+Be aware that the nodes meta-data tags are not strictly linked to these names, they are soft relations (this means that only the string matches, there is no any real relationship between them).
# Development
diff --git a/internal/consul/consul.go b/internal/consul/consul.go
index 3ba58b081..92a94421c 100644
--- a/internal/consul/consul.go
+++ b/internal/consul/consul.go
@@ -21,6 +21,7 @@ type Catalog interface {
type KV interface {
Get(key string, q *consulApi.QueryOptions) (*consulApi.KVPair, *consulApi.QueryMeta, error)
List(prefix string, q *consulApi.QueryOptions) (consulApi.KVPairs, *consulApi.QueryMeta, error)
+ Keys(prefix, separator string, q *consulApi.QueryOptions) ([]string, *consulApi.QueryMeta, error)
}
type Health interface {
diff --git a/internal/consul/mocks/KV.go b/internal/consul/mocks/KV.go
index b41cf1a88..1346f3c52 100644
--- a/internal/consul/mocks/KV.go
+++ b/internal/consul/mocks/KV.go
@@ -45,6 +45,38 @@ func (_m *KV) Get(key string, q *api.QueryOptions) (*api.KVPair, *api.QueryMeta,
return r0, r1, r2
}
+// Keys provides a mock function with given fields: prefix, separator, q
+func (_m *KV) Keys(prefix string, separator string, q *api.QueryOptions) ([]string, *api.QueryMeta, error) {
+ ret := _m.Called(prefix, separator, q)
+
+ var r0 []string
+ if rf, ok := ret.Get(0).(func(string, string, *api.QueryOptions) []string); ok {
+ r0 = rf(prefix, separator, q)
+ } else {
+ if ret.Get(0) != nil {
+ r0 = ret.Get(0).([]string)
+ }
+ }
+
+ var r1 *api.QueryMeta
+ if rf, ok := ret.Get(1).(func(string, string, *api.QueryOptions) *api.QueryMeta); ok {
+ r1 = rf(prefix, separator, q)
+ } else {
+ if ret.Get(1) != nil {
+ r1 = ret.Get(1).(*api.QueryMeta)
+ }
+ }
+
+ var r2 error
+ if rf, ok := ret.Get(2).(func(string, string, *api.QueryOptions) error); ok {
+ r2 = rf(prefix, separator, q)
+ } else {
+ r2 = ret.Error(2)
+ }
+
+ return r0, r1, r2
+}
+
// List provides a mock function with given fields: prefix, q
func (_m *KV) List(prefix string, q *api.QueryOptions) (api.KVPairs, *api.QueryMeta, error) {
ret := _m.Called(prefix, q)
diff --git a/web/app.go b/web/app.go
index 6854f0a01..338c9c300 100644
--- a/web/app.go
+++ b/web/app.go
@@ -57,6 +57,12 @@ func NewAppWithDeps(host string, port int, deps Dependencies) (*App, error) {
engine.GET("/hosts/:name/checks/:checkid", NewCheckHandler(deps.consul))
engine.GET("/clusters", NewClustersListHandler(deps.consul))
engine.GET("/clusters/:name", NewClusterHandler(deps.consul))
+ engine.GET("/environments", NewEnvironmentsListHandler(deps.consul))
+ engine.GET("/environments/:env", NewEnvironmentListHandler(deps.consul))
+ engine.GET("/landscapes", NewLandscapesListHandler(deps.consul))
+ engine.GET("/landscapes/:land", NewLandscapeListHandler(deps.consul))
+ engine.GET("/sapsystems", NewSAPSystemsListHandler(deps.consul))
+ engine.GET("/sapsystems/:sys", NewSAPSystemHostsListHandler(deps.consul))
apiGroup := engine.Group("/api")
{
diff --git a/web/environments.go b/web/environments.go
new file mode 100644
index 000000000..1b2255449
--- /dev/null
+++ b/web/environments.go
@@ -0,0 +1,330 @@
+package web
+
+import (
+ //"log"
+ "net/http"
+ "strings"
+
+ "github.com/gin-gonic/gin"
+ "github.com/pkg/errors"
+
+ "github.com/trento-project/trento/internal/consul"
+)
+
+const KVEnvironmentsPath string = "trento/environments"
+const envIndex int = 2
+const landIndex int = 4
+
+type EnvironmentHealth struct {
+ Health string
+ HealthMap map[string]string
+}
+
+func (e *EnvironmentHealth) updateHealth(n string, h string) EnvironmentHealth {
+ e.HealthMap[n] = h
+
+ if h == "critical" {
+ e.Health = h
+ } else if h == "warning" && e.Health != "critical" {
+ e.Health = h
+ }
+
+ return *e
+}
+
+type SAPSystem struct {
+ Name string
+ Hosts HostList
+}
+
+type SAPSystemList map[string]*SAPSystem
+
+func (s *SAPSystem) Health() EnvironmentHealth {
+ var health = EnvironmentHealth{
+ Health: "passing",
+ HealthMap: make(map[string]string),
+ }
+
+ for _, host := range s.Hosts {
+ h := host.Health()
+ health = health.updateHealth(host.Name(), h)
+ }
+
+ return health
+}
+
+type Landscape struct {
+ Name string
+ SAPSystems SAPSystemList
+}
+
+type LandscapeList map[string]*Landscape
+
+func (l *Landscape) Health() EnvironmentHealth {
+ var health = EnvironmentHealth{
+ Health: "passing",
+ HealthMap: make(map[string]string),
+ }
+
+ for _, system := range l.SAPSystems {
+ h := system.Health().Health
+ health = health.updateHealth(system.Name, h)
+ }
+
+ return health
+}
+
+type Environment struct {
+ Name string
+ Landscapes LandscapeList
+}
+
+type EnvironmentList map[string]*Environment
+
+func (l *Environment) Health() EnvironmentHealth {
+ var health = EnvironmentHealth{
+ Health: "passing",
+ HealthMap: make(map[string]string),
+ }
+
+ for _, land := range l.Landscapes {
+ h := land.Health().Health
+ health = health.updateHealth(land.Name, h)
+ }
+
+ return health
+}
+
+func NewEnvironmentsListHandler(client consul.Client) gin.HandlerFunc {
+ return func(c *gin.Context) {
+ environments, err := loadEnvironments(client)
+ if err != nil {
+ _ = c.Error(err)
+ return
+ }
+
+ c.HTML(http.StatusOK, "environments.html.tmpl", gin.H{
+ "Environments": environments,
+ })
+ }
+}
+
+func NewEnvironmentListHandler(client consul.Client) gin.HandlerFunc {
+ return func(c *gin.Context) {
+ environments, err := loadEnvironments(client)
+ if err != nil {
+ _ = c.Error(err)
+ return
+ }
+
+ c.HTML(http.StatusOK, "environment.html.tmpl", gin.H{
+ "Environments": environments,
+ "EnvName": c.Param("env"),
+ })
+ }
+}
+
+func NewLandscapesListHandler(client consul.Client) gin.HandlerFunc {
+ return func(c *gin.Context) {
+ var env string = ""
+
+ query := c.Request.URL.Query()
+ if len(query["environment"]) > 0 {
+ env = query["environment"][0]
+ }
+
+ environments, err := loadEnvironments(client)
+ if err != nil {
+ _ = c.Error(err)
+ return
+ }
+
+ c.HTML(http.StatusOK, "landscapes.html.tmpl", gin.H{
+ "Environments": environments,
+ "EnvName": env,
+ })
+ }
+}
+
+func NewLandscapeListHandler(client consul.Client) gin.HandlerFunc {
+ return func(c *gin.Context) {
+ var env string = ""
+
+ query := c.Request.URL.Query()
+ if len(query["environment"]) > 0 {
+ env = query["environment"][0]
+ }
+
+ environments, err := loadEnvironments(client)
+ if err != nil {
+ _ = c.Error(err)
+ return
+ }
+
+ c.HTML(http.StatusOK, "landscape.html.tmpl", gin.H{
+ "Environments": environments,
+ "EnvName": env,
+ "LandName": c.Param("land"),
+ })
+ }
+}
+
+func NewSAPSystemsListHandler(client consul.Client) gin.HandlerFunc {
+ return func(c *gin.Context) {
+ var env string = ""
+ var land string = ""
+
+ query := c.Request.URL.Query()
+ if len(query["environment"]) > 0 {
+ env = query["environment"][0]
+ }
+
+ if len(query["landscape"]) > 0 {
+ land = query["landscape"][0]
+ }
+
+ environments, err := loadEnvironments(client)
+ if err != nil {
+ _ = c.Error(err)
+ return
+ }
+
+ c.HTML(http.StatusOK, "sapsystems.html.tmpl", gin.H{
+ "Environments": environments,
+ "EnvName": env,
+ "LandName": land,
+ })
+ }
+}
+
+func NewSAPSystemHostsListHandler(client consul.Client) gin.HandlerFunc {
+ return func(c *gin.Context) {
+ var env string = ""
+ var land string = ""
+
+ query := c.Request.URL.Query()
+ if len(query["environment"]) > 0 {
+ env = query["environment"][0]
+ }
+
+ if len(query["landscape"]) > 0 {
+ land = query["landscape"][0]
+ }
+
+ environments, err := loadEnvironments(client)
+ if err != nil {
+ _ = c.Error(err)
+ return
+ }
+
+ c.HTML(http.StatusOK, "sapsystem.html.tmpl", gin.H{
+ "Environments": environments,
+ "EnvName": env,
+ "LandName": land,
+ "SAPSysName": c.Param("sys"),
+ })
+ }
+}
+
+/* loadEnvironments needs a fixed kv structure to work. Here an example
+trento/environments/
+trento/environments/env1/
+trento/environments/env1/landscapes/
+trento/environments/env1/landscapes/land1/
+trento/environments/env1/landscapes/land1/sapsystems/
+trento/environments/env1/landscapes/land1/sapsystems/sys1/
+trento/environments/env1/landscapes/land1/sapsystems/sys2/
+trento/environments/env1/landscapes/land2/
+trento/environments/env1/landscapes/land2/sapsystems/
+trento/environments/env1/landscapes/land2/sapsystems/sys3/
+trento/environments/env1/landscapes/land2/sapsystems/sys4/
+trento/environments/env2/
+trento/environments/env2/landscapes/
+trento/environments/env2/landscapes/land3/
+trento/environments/env2/landscapes/land3/sapsystems/
+trento/environments/env2/landscapes/land3/sapsystems/sys5/
+*/
+func loadEnvironments(client consul.Client) (EnvironmentList, error) {
+ var (
+ environments = EnvironmentList{}
+ reserveKeys = []string{"environments", "landscapes", "sapsystems"}
+ )
+
+ entries, _, err := client.KV().Keys(KVEnvironmentsPath, "", nil)
+ if err != nil {
+ return nil, errors.Wrap(err, "could not query Consul for Environments KV values")
+ }
+
+ for _, entry := range entries {
+ // Remove individual values, even though there is not any defined by now.
+ if !strings.HasSuffix(entry, "/") {
+ continue
+ }
+
+ keyValues := strings.Split(strings.TrimSuffix(entry, "/"), "/")
+ lastKey := keyValues[len(keyValues)-1]
+ lastKeyParent := keyValues[len(keyValues)-2]
+
+ if contains(reserveKeys, lastKey) {
+ continue
+ }
+
+ _, envFound := environments[lastKeyParent]
+ if lastKeyParent == "environments" && !envFound {
+ env := &Environment{Name: lastKey, Landscapes: make(LandscapeList)}
+ environments[lastKey] = env
+ }
+
+ environments, err = loadLandscapes(client, environments, keyValues)
+ if err != nil {
+ return nil, errors.Wrap(err, "could not get the SAP landscapes")
+ }
+ }
+
+ return environments, nil
+}
+
+func loadLandscapes(client consul.Client, environments EnvironmentList, values []string) (EnvironmentList, error) {
+ lastKey := values[len(values)-1]
+ lastKeyParent := values[len(values)-2]
+
+ _, landFound := environments[lastKeyParent]
+ if lastKeyParent == "landscapes" && !landFound {
+ land := &Landscape{Name: lastKey, SAPSystems: make(SAPSystemList)}
+ envName := values[envIndex]
+ environments[envName].Landscapes[lastKey] = land
+ }
+
+ environments, err := loadSAPSystems(client, environments, values)
+ if err != nil {
+ return nil, errors.Wrap(err, "could not get the SAP systems")
+ }
+
+ return environments, nil
+}
+
+func loadSAPSystems(client consul.Client, environments EnvironmentList, values []string) (EnvironmentList, error) {
+ lastKey := values[len(values)-1]
+ lastKeyParent := values[len(values)-2]
+
+ _, sysFound := environments[lastKeyParent]
+ if lastKeyParent == "sapsystems" && !sysFound {
+ envName := values[envIndex]
+ landName := values[landIndex]
+ // Get the nodes with these meta-data entries
+ query := CreateFilterMetaQuery(map[string][]string{
+ "trento-sap-environment": []string{envName},
+ "trento-sap-landscape": []string{landName},
+ "trento-sap-system": []string{lastKey},
+ })
+ hosts, err := loadHosts(client, query, []string{})
+ if err != nil {
+ return nil, errors.Wrap(err, "could not query Consul for hosts")
+ }
+ sapsystem := &SAPSystem{Name: lastKey, Hosts: hosts}
+
+ environments[envName].Landscapes[landName].SAPSystems[lastKey] = sapsystem
+ }
+
+ return environments, nil
+}
diff --git a/web/environments_test.go b/web/environments_test.go
new file mode 100644
index 000000000..8f8098135
--- /dev/null
+++ b/web/environments_test.go
@@ -0,0 +1,245 @@
+package web
+
+import (
+ "net/http"
+ "net/http/httptest"
+ "regexp"
+ "testing"
+
+ consulApi "github.com/hashicorp/consul/api"
+ "github.com/stretchr/testify/assert"
+ "github.com/tdewolff/minify/v2"
+ "github.com/tdewolff/minify/v2/html"
+ "github.com/trento-project/trento/internal/consul/mocks"
+)
+
+func setupTest() (*mocks.Client, *mocks.Catalog) {
+ nodes1 := []*consulApi.Node{
+ {
+ Node: "node1",
+ Datacenter: "dc",
+ Address: "192.168.1.1",
+ Meta: map[string]string{
+ "trento-sap-environments": "land1",
+ },
+ },
+ {
+ Node: "node2",
+ Datacenter: "dc",
+ Address: "192.168.1.2",
+ Meta: map[string]string{
+ "trento-sap-environments": "land1",
+ },
+ },
+ }
+
+ nodes2 := []*consulApi.Node{
+ {
+ Node: "node3",
+ Datacenter: "dc1",
+ Address: "192.168.1.2",
+ Meta: map[string]string{
+ "trento-sap-environments": "land2",
+ },
+ },
+ {
+ Node: "node4",
+ Datacenter: "dc",
+ Address: "192.168.1.3",
+ Meta: map[string]string{
+ "trento-sap-environments": "land2",
+ },
+ },
+ }
+
+ node1HealthChecks := consulApi.HealthChecks{
+ &consulApi.HealthCheck{
+ Status: consulApi.HealthPassing,
+ },
+ }
+
+ node2HealthChecks := consulApi.HealthChecks{
+ &consulApi.HealthCheck{
+ Status: consulApi.HealthPassing,
+ },
+ }
+
+ node3HealthChecks := consulApi.HealthChecks{
+ &consulApi.HealthCheck{
+ Status: consulApi.HealthPassing,
+ },
+ }
+
+ node4HealthChecks := consulApi.HealthChecks{
+ &consulApi.HealthCheck{
+ Status: consulApi.HealthCritical,
+ },
+ }
+
+ filters := []string{
+ "trento/environments/",
+ "trento/environments/env1/",
+ "trento/environments/env1/landscapes/",
+ "trento/environments/env1/landscapes/land1/",
+ "trento/environments/env1/landscapes/land1/sapsystems/",
+ "trento/environments/env1/landscapes/land1/sapsystems/sys1/",
+ "trento/environments/env1/landscapes/land2/",
+ "trento/environments/env1/landscapes/land2/sapsystems/",
+ "trento/environments/env1/landscapes/land2/sapsystems/sys2/",
+ "trento/environments/env2/",
+ "trento/environments/env2/landscapes/",
+ "trento/environments/env2/landscapes/land3/",
+ "trento/environments/env2/landscapes/land3/sapsystems/",
+ "trento/environments/env2/landscapes/land3/sapsystems/sys3/",
+ }
+
+ consul := new(mocks.Client)
+ catalog := new(mocks.Catalog)
+ health := new(mocks.Health)
+ kv := new(mocks.KV)
+
+ consul.On("Catalog").Return(catalog)
+ consul.On("Health").Return(health)
+ consul.On("KV").Return(kv)
+
+ kv.On("Keys", "trento/environments", "", (*consulApi.QueryOptions)(nil)).Return(filters, nil, nil)
+
+ filterSys1 := &consulApi.QueryOptions{
+ Filter: "(Meta[\"trento-sap-environment\"] == \"env1\") and (Meta[\"trento-sap-landscape\"] == \"land1\") and (Meta[\"trento-sap-system\"] == \"sys1\")"}
+ catalog.On("Nodes", (filterSys1)).Return(nodes1, nil, nil)
+
+ filterSys2 := &consulApi.QueryOptions{
+ Filter: "(Meta[\"trento-sap-environment\"] == \"env1\") and (Meta[\"trento-sap-landscape\"] == \"land2\") and (Meta[\"trento-sap-system\"] == \"sys2\")"}
+ catalog.On("Nodes", (filterSys2)).Return(nodes1, nil, nil)
+
+ filterSys3 := &consulApi.QueryOptions{
+ Filter: "(Meta[\"trento-sap-environment\"] == \"env2\") and (Meta[\"trento-sap-landscape\"] == \"land3\") and (Meta[\"trento-sap-system\"] == \"sys3\")"}
+ catalog.On("Nodes", (filterSys3)).Return(nodes2, nil, nil)
+
+ health.On("Node", "node1", (*consulApi.QueryOptions)(nil)).Return(node1HealthChecks, nil, nil)
+ health.On("Node", "node2", (*consulApi.QueryOptions)(nil)).Return(node2HealthChecks, nil, nil)
+ health.On("Node", "node3", (*consulApi.QueryOptions)(nil)).Return(node3HealthChecks, nil, nil)
+ health.On("Node", "node4", (*consulApi.QueryOptions)(nil)).Return(node4HealthChecks, nil, nil)
+
+ return consul, catalog
+}
+
+func TestEnvironmentsListHandler(t *testing.T) {
+ consul, catalog := setupTest()
+
+ deps := DefaultDependencies()
+ deps.consul = consul
+
+ var err error
+ app, err := NewAppWithDeps("", 80, deps)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ resp := httptest.NewRecorder()
+ req, err := http.NewRequest("GET", "/environments", nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ app.ServeHTTP(resp, req)
+
+ consul.AssertExpectations(t)
+ catalog.AssertExpectations(t)
+
+ m := minify.New()
+ m.AddFunc("text/html", html.Minify)
+ m.Add("text/html", &html.Minifier{
+ KeepDefaultAttrVals: true,
+ KeepEndTags: true,
+ })
+ minified, err := m.String("text/html", resp.Body.String())
+ if err != nil {
+ panic(err)
+ }
+
+ assert.Equal(t, 200, resp.Code)
+ assert.Contains(t, minified, "Environments")
+ assert.Regexp(t, regexp.MustCompile("
env12 2 .*passing.* "), minified)
+ assert.Regexp(t, regexp.MustCompile("env21 1 .*critical.* "), minified)
+}
+
+func TestLandscapesListHandler(t *testing.T) {
+ consul, catalog := setupTest()
+
+ deps := DefaultDependencies()
+ deps.consul = consul
+
+ var err error
+ app, err := NewAppWithDeps("", 80, deps)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ resp := httptest.NewRecorder()
+ req, err := http.NewRequest("GET", "/landscapes", nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ app.ServeHTTP(resp, req)
+
+ consul.AssertExpectations(t)
+ catalog.AssertExpectations(t)
+
+ m := minify.New()
+ m.AddFunc("text/html", html.Minify)
+ m.Add("text/html", &html.Minifier{
+ KeepDefaultAttrVals: true,
+ KeepEndTags: true,
+ })
+ minified, err := m.String("text/html", resp.Body.String())
+ if err != nil {
+ panic(err)
+ }
+
+ assert.Equal(t, 200, resp.Code)
+ assert.Regexp(t, regexp.MustCompile("land1 .*env1.* 1 2 .*passing.* "), minified)
+ assert.Regexp(t, regexp.MustCompile("land2.*env1.* 1 2 .*passing.* "), minified)
+ assert.Regexp(t, regexp.MustCompile("land3.*env2.* 1 2 .*critical.* "), minified)
+}
+
+func TestSAPSystemsListHandler(t *testing.T) {
+ consul, catalog := setupTest()
+
+ deps := DefaultDependencies()
+ deps.consul = consul
+
+ var err error
+ app, err := NewAppWithDeps("", 80, deps)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ resp := httptest.NewRecorder()
+ req, err := http.NewRequest("GET", "/sapsystems", nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ app.ServeHTTP(resp, req)
+
+ consul.AssertExpectations(t)
+ catalog.AssertExpectations(t)
+
+ m := minify.New()
+ m.AddFunc("text/html", html.Minify)
+ m.Add("text/html", &html.Minifier{
+ KeepDefaultAttrVals: true,
+ KeepEndTags: true,
+ })
+ minified, err := m.String("text/html", resp.Body.String())
+ if err != nil {
+ panic(err)
+ }
+
+ assert.Equal(t, 200, resp.Code)
+ assert.Regexp(t, regexp.MustCompile("sys1.*passing.* "), minified)
+ assert.Regexp(t, regexp.MustCompile("sys2.*passing.* "), minified)
+ assert.Regexp(t, regexp.MustCompile("sys3.*critical.* "), minified)
+}
diff --git a/web/hosts.go b/web/hosts.go
index a58753441..68ecf5fff 100644
--- a/web/hosts.go
+++ b/web/hosts.go
@@ -6,6 +6,7 @@ import (
"io"
"log"
"net/http"
+ "sort"
"strings"
"github.com/aquasecurity/bench-common/check"
@@ -16,12 +17,7 @@ import (
"github.com/trento-project/trento/internal/consul"
)
-const TRENTO_PREFIX string = "trento-"
-const TRENTO_FILTERS_PREFIX string = "trento/filters/"
-
-func TRENTO_FILTERS() []string {
- return []string{"sap-environments", "sap-landscapes", "sap-systems"}
-}
+const TrentoPrefix string = "trento-"
type HostList []*Host
@@ -43,7 +39,7 @@ func (n *Host) TrentoMeta() map[string]string {
filtered_meta := make(map[string]string)
for key, value := range n.Node.Meta {
- if strings.HasPrefix(key, TRENTO_PREFIX) {
+ if strings.HasPrefix(key, TrentoPrefix) {
filtered_meta[key] = value
}
}
@@ -76,23 +72,35 @@ func (n *Host) Checks() *check.Controls {
return checks
}
+func sortKeys(m map[string][]string) []string {
+ var keys []string
+ for k := range m {
+ keys = append(keys, k)
+ }
+ sort.Strings(keys)
+ return keys
+}
+
// Use github.com/hashicorp/go-bexpr to create the filter
// https://github.com/hashicorp/consul/blob/master/agent/consul/catalog_endpoint.go#L298
func CreateFilterMetaQuery(query map[string][]string) string {
var filters []string
+ // Need to sort the keys to have stable output. Mostly for unit testing
+ sortedQuery := sortKeys(query)
if len(query) != 0 {
var filter string
- for key, values := range query {
- if strings.HasPrefix(key, TRENTO_PREFIX) {
+ for _, key := range sortedQuery {
+ if strings.HasPrefix(key, TrentoPrefix) {
filter = ""
+ values := query[key]
for _, value := range values {
filter = fmt.Sprintf("%sMeta[\"%s\"] == \"%s\"", filter, key, value)
if values[len(values)-1] != value {
filter = fmt.Sprintf("%s or ", filter)
}
}
- filters = append(filters, filter)
+ filters = append(filters, fmt.Sprintf("(%s)", filter))
}
}
}
@@ -125,16 +133,6 @@ func NewHostsListHandler(client consul.Client) gin.HandlerFunc {
}
}
-func contains(s []string, str string) bool {
- for _, v := range s {
- if v == str {
- return true
- }
- }
-
- return false
-}
-
func loadHosts(client consul.Client, query_filter string, health_filter []string) (HostList, error) {
var hosts = HostList{}
@@ -154,32 +152,28 @@ func loadHosts(client consul.Client, query_filter string, health_filter []string
return hosts, nil
}
-func loadFilter(client consul.Client, filter string) ([]string, error) {
- filters, _, err := client.KV().Get(filter, nil)
- if filters == nil {
- return nil, errors.Wrap(err, "could not query Consul for filters on the KV storage")
- }
+func loadFilters(client consul.Client) (map[string][]string, error) {
+ filter_data := make(map[string][]string)
- var unmarshalled []string
- if err := json.Unmarshal([]byte(string(filters.Value)), &unmarshalled); err != nil {
- return nil, errors.Wrap(err, "error decoding the filter data")
+ environments, err := loadEnvironments(client)
+ if err != nil {
+ return nil, errors.Wrap(err, "could not get the filters")
}
- return unmarshalled, nil
-}
-
-func loadFilters(client consul.Client) (map[string][]string, error) {
- //We could use the kV().List to get all the filters too
- //_, _, _ := client.KV().List("trento/filters/", nil)
- filter_data := make(map[string][]string)
- for _, filter := range TRENTO_FILTERS() {
- filters, err := loadFilter(client, TRENTO_FILTERS_PREFIX+filter)
- if err != nil {
- return nil, err
+ for envKey, envValue := range environments {
+ filter_data["environments"] = append(filter_data["environments"], envKey)
+ for landKey, landValue := range envValue.Landscapes {
+ filter_data["landscapes"] = append(filter_data["landscapes"], landKey)
+ for sysKey, _ := range landValue.SAPSystems {
+ filter_data["sapsystems"] = append(filter_data["sapsystems"], sysKey)
+ }
}
- filter_data[filter] = filters
}
+ sort.Strings(filter_data["environments"])
+ sort.Strings(filter_data["landscapes"])
+ sort.Strings(filter_data["sapsystems"])
+
return filter_data, nil
}
diff --git a/web/hosts_test.go b/web/hosts_test.go
index 65a5a5fa3..54741ff35 100644
--- a/web/hosts_test.go
+++ b/web/hosts_test.go
@@ -45,14 +45,21 @@ func TestHostsListHandler(t *testing.T) {
},
}
- filterEnv := &consulApi.KVPair{
- Value: []byte("[\"env1\", \"env2\"]"),
- }
- filterLand := &consulApi.KVPair{
- Value: []byte("[\"land1\", \"land2\"]"),
- }
- filterSys := &consulApi.KVPair{
- Value: []byte("[\"sys1\", \"sys2\"]"),
+ filters := []string{
+ "trento/environments/",
+ "trento/environments/env1/",
+ "trento/environments/env1/landscapes/",
+ "trento/environments/env1/landscapes/land1/",
+ "trento/environments/env1/landscapes/land1/sapsystems/",
+ "trento/environments/env1/landscapes/land1/sapsystems/sys1/",
+ "trento/environments/env1/landscapes/land2/",
+ "trento/environments/env1/landscapes/land2/sapsystems/",
+ "trento/environments/env1/landscapes/land2/sapsystems/sys2/",
+ "trento/environments/env2/",
+ "trento/environments/env2/landscapes/",
+ "trento/environments/env2/landscapes/land3/",
+ "trento/environments/env2/landscapes/land3/sapsystems/",
+ "trento/environments/env2/landscapes/land3/sapsystems/sys3/",
}
consul := new(mocks.Client)
@@ -64,16 +71,26 @@ func TestHostsListHandler(t *testing.T) {
consul.On("Health").Return(health)
consul.On("KV").Return(kv)
+ kv.On("Keys", "trento/environments", "", (*consulApi.QueryOptions)(nil)).Return(filters, nil, nil)
+
query := &consulApi.QueryOptions{Filter: ""}
catalog.On("Nodes", (*consulApi.QueryOptions)(query)).Return(nodes, nil, nil)
+ filterSys1 := &consulApi.QueryOptions{
+ Filter: "(Meta[\"trento-sap-environment\"] == \"env1\") and (Meta[\"trento-sap-landscape\"] == \"land1\") and (Meta[\"trento-sap-system\"] == \"sys1\")"}
+ catalog.On("Nodes", (filterSys1)).Return(nodes, nil, nil)
+
+ filterSys2 := &consulApi.QueryOptions{
+ Filter: "(Meta[\"trento-sap-environment\"] == \"env1\") and (Meta[\"trento-sap-landscape\"] == \"land2\") and (Meta[\"trento-sap-system\"] == \"sys2\")"}
+ catalog.On("Nodes", (filterSys2)).Return(nodes, nil, nil)
+
+ filterSys3 := &consulApi.QueryOptions{
+ Filter: "(Meta[\"trento-sap-environment\"] == \"env2\") and (Meta[\"trento-sap-landscape\"] == \"land3\") and (Meta[\"trento-sap-system\"] == \"sys3\")"}
+ catalog.On("Nodes", (filterSys3)).Return(nodes, nil, nil)
+
health.On("Node", "foo", (*consulApi.QueryOptions)(nil)).Return(fooHealthChecks, nil, nil)
health.On("Node", "bar", (*consulApi.QueryOptions)(nil)).Return(barHealthChecks, nil, nil)
- kv.On("Get", "trento/filters/sap-environments", (*consulApi.QueryOptions)(nil)).Return(filterEnv, nil, nil)
- kv.On("Get", "trento/filters/sap-landscapes", (*consulApi.QueryOptions)(nil)).Return(filterLand, nil, nil)
- kv.On("Get", "trento/filters/sap-systems", (*consulApi.QueryOptions)(nil)).Return(filterSys, nil, nil)
-
deps := DefaultDependencies()
deps.consul = consul
@@ -109,8 +126,8 @@ func TestHostsListHandler(t *testing.T) {
assert.Equal(t, 200, resp.Code)
assert.Contains(t, minified, "Hosts")
assert.Regexp(t, regexp.MustCompile(".*env1.*env2.* "), minified)
- assert.Regexp(t, regexp.MustCompile(".*land1.*land2.* "), minified)
- assert.Regexp(t, regexp.MustCompile(".*sys1.*sys2.* "), minified)
+ assert.Regexp(t, regexp.MustCompile(".*land1.*land2.*land3.* "), minified)
+ assert.Regexp(t, regexp.MustCompile(".*sys1.*sys2.*sys3.* "), minified)
assert.Regexp(t, regexp.MustCompile("foo 192.168.1.1 .*land1.* .*passing.* "), minified)
assert.Regexp(t, regexp.MustCompile("bar 192.168.1.2 .*land2.* .*critical.* "), minified)
}
diff --git a/web/layout.go b/web/layout.go
index 3168b3ce8..1d6bf1293 100644
--- a/web/layout.go
+++ b/web/layout.go
@@ -90,6 +90,9 @@ func (r *LayoutRender) addFileFromFS(templatesFS fs.FS, file string) {
_ = tmpl.ExecuteTemplate(&out, name, data)
return out.String()
},
+ "sum": func(a int, b int) int {
+ return a + b
+ },
})
patterns := append([]string{r.root, file}, r.blocks...)
tmpl = template.Must(tmpl.ParseFS(templatesFS, patterns...))
diff --git a/web/templates/blocks/landscapes_row.html.tmpl b/web/templates/blocks/landscapes_row.html.tmpl
new file mode 100644
index 000000000..a7da0c825
--- /dev/null
+++ b/web/templates/blocks/landscapes_row.html.tmpl
@@ -0,0 +1,22 @@
+{{ define "landscapes_row" }}
+{{- $EnvName := .Name }}
+{{- range .Landscapes }}
+
+ {{ .Name }}
+ {{ $EnvName }}
+ {{- $SAPSystemNumber := 0 }}
+ {{- $HostsNumber := 0 }}
+ {{- $SAPSystemNumber = sum $SAPSystemNumber (len .SAPSystems) }}
+ {{- range .SAPSystems }}
+ {{- $HostsNumber = sum $HostsNumber (len .Hosts) }}
+ {{- end }}
+ {{ $SAPSystemNumber }}
+ {{ $HostsNumber }}
+
+ {{- $Health := .Health }}
+ {{- /* It would be nice to show the summary of the the health as tooltip. How many passing, critical and warning in a nice an visual way */ -}}
+ {{ $Health.Health }}
+
+
+{{- end }}
+{{ end }}
diff --git a/web/templates/blocks/landscapes_table.html.tmpl b/web/templates/blocks/landscapes_table.html.tmpl
new file mode 100644
index 000000000..c641d67d5
--- /dev/null
+++ b/web/templates/blocks/landscapes_table.html.tmpl
@@ -0,0 +1,24 @@
+{{ define "landscapes_table" }}
+
+
+
+ Landscape
+ Environment
+ SAP systems number
+ Hosts number
+ Status
+
+
+
+ {{- if .EnvName }}
+ {{ $EnvName := .EnvName }}
+ {{ $Env := index .Environments .EnvName }}
+ {{ template "landscapes_row" $Env }}
+ {{- else }}
+ {{- range .Environments }}
+ {{ template "landscapes_row" . }}
+ {{- end }}
+ {{- end }}
+
+
+{{ end }}
diff --git a/web/templates/blocks/sapsystems_table.html.tmpl b/web/templates/blocks/sapsystems_table.html.tmpl
new file mode 100644
index 000000000..4a5f48ff9
--- /dev/null
+++ b/web/templates/blocks/sapsystems_table.html.tmpl
@@ -0,0 +1,56 @@
+{{ define "sapsystems_table" }}
+
+
+
+
+ SAP System
+ Environment
+ Landscape
+ Hosts number
+ Status
+
+
+
+ {{- if .EnvName }}
+ {{ $EnvName := .EnvName }}
+ {{ $LandName := .LandName }}
+ {{ $Env := index .Environments .EnvName }}
+ {{ $Land := index $Env.Landscapes .LandName }}
+ {{- range $Land.SAPSystems }}
+
+ {{ .Name }}
+ {{ $EnvName }}
+ {{ $EnvName }}
+ {{ len .Hosts }}
+
+ {{- $Health := .Health }}
+ {{- /* It would be nice to show the summary of the the health as tooltip. How many passing, critical and warning in a nice an visual way */ -}}
+ {{ $Health.Health }}
+
+
+ {{- end }}
+ {{- else }}
+ {{- range .Environments }}
+ {{- $EnvName := .Name }}
+ {{- range .Landscapes }}
+ {{- $LandName := .Name }}
+ {{- range .SAPSystems }}
+
+ {{ .Name }}
+ {{ $EnvName }}
+ {{ $LandName }}
+ {{ len .Hosts }}
+
+ {{- $Health := .Health }}
+ {{- /* It would be nice to show the summary of the the health as tooltip. How many passing, critical and warning in a nice an visual way */ -}}
+ {{ $Health.Health }}
+
+
+ {{- end }}
+ {{- end }}
+ {{- end }}
+ {{- end }}
+
+
+
+{{ end }}
diff --git a/web/templates/blocks/sidebar.html.tmpl b/web/templates/blocks/sidebar.html.tmpl
index 181a493dd..d4fb35afe 100644
--- a/web/templates/blocks/sidebar.html.tmpl
+++ b/web/templates/blocks/sidebar.html.tmpl
@@ -27,6 +27,18 @@
collocation
+
+
+
diff --git a/web/templates/cluster.html.tmpl b/web/templates/cluster.html.tmpl
index fb832ed6b..d5f242d53 100644
--- a/web/templates/cluster.html.tmpl
+++ b/web/templates/cluster.html.tmpl
@@ -1,7 +1,7 @@
{{ define "content" }}
-
Clusters > {{ .Cluster.Name }}
-
Cluster details
+
Clusters > {{ .Cluster.Name }}
+
Cluster details
Name
{{ .Cluster.Name }}
diff --git a/web/templates/environment.html.tmpl b/web/templates/environment.html.tmpl
new file mode 100644
index 000000000..c2d91b2ed
--- /dev/null
+++ b/web/templates/environment.html.tmpl
@@ -0,0 +1,15 @@
+{{ define "content" }}
+
+
Environments
+ Environment details
+
+ Name
+ {{ .EnvName }}
+
+
+
+
Landscapes
+ {{ template "landscapes_table" . }}
+
+
+{{ end }}
diff --git a/web/templates/environments.html.tmpl b/web/templates/environments.html.tmpl
new file mode 100644
index 000000000..225ea4cc0
--- /dev/null
+++ b/web/templates/environments.html.tmpl
@@ -0,0 +1,41 @@
+{{ define "content" }}
+
+
Environments
+
+
+
+
+ Environment
+ Landscapes number
+ SAP systems number
+ Hosts number
+ Status
+
+
+
+ {{- range .Environments }}
+
+ {{ .Name }}
+ {{ len .Landscapes }}
+ {{- $SAPSystemNumber := 0 }}
+ {{- $HostsNumber := 0 }}
+ {{- range .Landscapes }}
+ {{- $SAPSystemNumber = sum $SAPSystemNumber (len .SAPSystems) }}
+ {{- range .SAPSystems }}
+ {{- $HostsNumber = sum $HostsNumber (len .Hosts) }}
+ {{- end }}
+ {{- end }}
+ {{ $SAPSystemNumber }}
+ {{ $HostsNumber }}
+
+ {{- $Health := .Health }}
+ {{- /* I`t would be nice to show the summary of the the health as tooltip. How many passing, critical and warning in a nice an visual way */ -}}
+ {{ $Health.Health }}
+
+
+ {{- end }}
+
+
+
+
+{{ end }}
diff --git a/web/templates/ha_checks.html.tmpl b/web/templates/ha_checks.html.tmpl
index cff81868c..da1fd82dd 100644
--- a/web/templates/ha_checks.html.tmpl
+++ b/web/templates/ha_checks.html.tmpl
@@ -1,8 +1,7 @@
{{ define "content" }}
-
-
-
{{ .CheckContent.Description }}
+
+
{{ .CheckContent.Description }}
diff --git a/web/templates/host.html.tmpl b/web/templates/host.html.tmpl
index 8912c0b09..143d15295 100644
--- a/web/templates/host.html.tmpl
+++ b/web/templates/host.html.tmpl
@@ -1,10 +1,19 @@
{{ define "content" }}
-
Hosts > {{ .Host.Name }}
-
Host details
+
Hosts > {{ .Host.Name }}
+
Host details
Name
{{ .Host.Name }}
+ {{ $Env := index .Host.TrentoMeta "trento-sap-environment" }}
+ {{ $Land := index .Host.TrentoMeta "trento-sap-landscape" }}
+ {{ $SAPSys := index .Host.TrentoMeta "trento-sap-system" }}
+ Environment
+ {{ $Env }}
+ Landscape
+ {{ $Land }}
+ SAP System
+ {{ $SAPSys }}
Cluster
{{ index .Host.TrentoMeta "trento-ha-cluster" }}
diff --git a/web/templates/hosts.html.tmpl b/web/templates/hosts.html.tmpl
index 96da1315a..254318d54 100644
--- a/web/templates/hosts.html.tmpl
+++ b/web/templates/hosts.html.tmpl
@@ -14,18 +14,18 @@