[VOL-3678] First implementation of the BBSim-sadis-server

Change-Id: I5077a8f861f4cc6af9759f31a4a415042c05eba3
diff --git a/internal/core/common.go b/internal/core/common.go
new file mode 100644
index 0000000..0d360ad
--- /dev/null
+++ b/internal/core/common.go
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+
+ * http://www.apache.org/licenses/LICENSE-2.0
+
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package core
+
+import "github.com/opencord/voltha-lib-go/v4/pkg/log"
+
+var logger log.CLogger
+
+func SetupLogger(logLevel log.LogLevel, logFormat string) {
+	// Setup this package so that it's log level can be modified at run time
+	var err error
+	logger, err = log.RegisterPackage(logFormat, logLevel, log.Fields{})
+	if err != nil {
+		panic(err)
+	}
+}
diff --git a/internal/core/sadis_if.go b/internal/core/sadis_if.go
new file mode 100644
index 0000000..3b5abfb
--- /dev/null
+++ b/internal/core/sadis_if.go
@@ -0,0 +1,103 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+
+ * http://www.apache.org/licenses/LICENSE-2.0
+
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package core
+
+// TODO this should be imported from github.com/opencord/bbsim
+// but to do that we need to move them from the "internal" module
+
+type SadisConfig struct {
+	Sadis            SadisEntries            `json:"sadis"`
+	BandwidthProfile BandwidthProfileEntries `json:"bandwidthprofile"`
+}
+
+type SadisEntries struct {
+	Integration SadisIntegration `json:"integration"`
+	Entries     []*SadisEntry    `json:"entries,omitempty"`
+	//Entries     []interface{}    `json:"entries,omitempty"`
+}
+
+type BandwidthProfileEntries struct {
+	Integration SadisIntegration `json:"integration"`
+	Entries     []*SadisBWPEntry `json:"entries,omitempty"`
+}
+
+type SadisIntegration struct {
+	URL   string `json:"url,omitempty"`
+	Cache struct {
+		Enabled bool   `json:"enabled"`
+		MaxSize int    `json:"maxsize"`
+		TTL     string `json:"ttl"`
+	} `json:"cache"`
+}
+
+type SadisEntry struct {
+	// common
+	ID string `json:"id"`
+	// olt
+	HardwareIdentifier string `json:"hardwareIdentifier"`
+	IPAddress          string `json:"ipAddress"`
+	NasID              string `json:"nasId"`
+	UplinkPort         int    `json:"uplinkPort"`
+	// onu
+	NasPortID  string        `json:"nasPortId"`
+	CircuitID  string        `json:"circuitId"`
+	RemoteID   string        `json:"remoteId"`
+	UniTagList []SadisUniTag `json:"uniTagList"`
+}
+
+type SadisOltEntry struct {
+	ID                 string `json:"id"`
+	HardwareIdentifier string `json:"hardwareIdentifier"`
+	IPAddress          string `json:"ipAddress"`
+	NasID              string `json:"nasId"`
+	UplinkPort         int    `json:"uplinkPort"`
+}
+
+type SadisOnuEntryV2 struct {
+	ID         string        `json:"id"`
+	NasPortID  string        `json:"nasPortId"`
+	CircuitID  string        `json:"circuitId"`
+	RemoteID   string        `json:"remoteId"`
+	UniTagList []SadisUniTag `json:"uniTagList"` // this can be SadisUniTagAtt, SadisUniTagDt
+}
+
+type SadisUniTag struct {
+	UniTagMatch                int    `json:"uniTagMatch,omitempty"`
+	PonCTag                    int    `json:"ponCTag,omitempty"`
+	PonSTag                    int    `json:"ponSTag,omitempty"`
+	TechnologyProfileID        int    `json:"technologyProfileId,omitempty"`
+	UpstreamBandwidthProfile   string `json:"upstreamBandwidthProfile,omitempty"`
+	DownstreamBandwidthProfile string `json:"downstreamBandwidthProfile,omitempty"`
+	IsDhcpRequired             bool   `json:"isDhcpRequired,omitempty"`
+	IsIgmpRequired             bool   `json:"isIgmpRequired,omitempty"`
+	ConfiguredMacAddress       string `json:"configuredMacAddress,omitempty"`
+	UsPonCTagPriority          uint8  `json:"usPonCTagPriority,omitempty"`
+	UsPonSTagPriority          uint8  `json:"usPonSTagPriority,omitempty"`
+	DsPonCTagPriority          uint8  `json:"dsPonCTagPriority,omitempty"`
+	DsPonSTagPriority          uint8  `json:"dsPonSTagPriority,omitempty"`
+	ServiceName                string `json:"serviceName,omitempty"`
+}
+
+// SADIS BandwithProfile Entry
+type SadisBWPEntry struct {
+	ID  string `json:"id"`
+	AIR int    `json:"air"`
+	CBS int    `json:"cbs"`
+	CIR int    `json:"cir"`
+	EBS int    `json:"ebs"`
+	EIR int    `json:"eir"`
+}
diff --git a/internal/core/server.go b/internal/core/server.go
new file mode 100644
index 0000000..8a5dfd6
--- /dev/null
+++ b/internal/core/server.go
@@ -0,0 +1,107 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+
+ * http://www.apache.org/licenses/LICENSE-2.0
+
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package core
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+	"github.com/gorilla/mux"
+	"github.com/opencord/voltha-lib-go/v4/pkg/log"
+	"net/http"
+	"sync"
+)
+
+type Server struct {
+	store *Store
+}
+
+func NewServer(store *Store) *Server {
+	return &Server{
+		store: store,
+	}
+}
+
+func (s *Server) StartSadisServer(wg *sync.WaitGroup) {
+	defer wg.Done()
+	ctx := context.Background()
+
+	addr := "0.0.0.0:8080"
+
+	router := mux.NewRouter().StrictSlash(true)
+	router.HandleFunc("/subscribers/{ID}", s.serveEntry)
+	router.HandleFunc("/profiles/{ID}", s.serveBWPEntry)
+
+	logger.Fatal(ctx, http.ListenAndServe(addr, router))
+}
+
+func (s Server) serveEntry(w http.ResponseWriter, r *http.Request) {
+	vars := mux.Vars(r)
+	id := vars["ID"]
+
+	ctx := context.TODO()
+	logger.Debugw(ctx, "received-sadis-entry-request", log.Fields{"id": id})
+
+	w.Header().Set("Content-Type", "application/json")
+
+	if olt, err := s.store.getOlt(r.Context(), id); err == nil {
+		w.WriteHeader(http.StatusOK)
+		_ = json.NewEncoder(w).Encode(olt)
+		logger.Infow(ctx, "responded-to-sadis-olt-entry-request", log.Fields{"id": id})
+		return
+	}
+
+	if onu, err := s.store.getOnu(r.Context(), id); err == nil {
+		w.WriteHeader(http.StatusOK)
+		_ = json.NewEncoder(w).Encode(onu)
+		logger.Infow(ctx, "responded-to-sadis-onu-entry-request", log.Fields{"id": id})
+		return
+	}
+
+	w.WriteHeader(http.StatusNotFound)
+	msg := make(map[string]interface{})
+	msg["statusCode"] = http.StatusNotFound
+	msg["message"] = fmt.Sprintf("Entry with ID %s not found.", id)
+	_ = json.NewEncoder(w).Encode(msg)
+
+	logger.Warnw(ctx, "sadis-entry-not-found", log.Fields{"id": id})
+}
+
+func (s Server) serveBWPEntry(w http.ResponseWriter, r *http.Request) {
+	vars := mux.Vars(r)
+	id := vars["ID"]
+
+	ctx := context.TODO()
+	logger.Debugw(ctx, "received-sadis-bandwidthprofile-request", log.Fields{"id": id})
+
+	w.Header().Set("Content-Type", "application/json")
+
+	if bp, err := s.store.getBp(r.Context(), id); err == nil {
+		w.WriteHeader(http.StatusOK)
+		_ = json.NewEncoder(w).Encode(bp)
+		logger.Infow(ctx, "responded-to-sadis-bandwidthprofile-request", log.Fields{"id": id})
+		return
+	}
+
+	w.WriteHeader(http.StatusNotFound)
+	msg := make(map[string]interface{})
+	msg["statusCode"] = http.StatusNotFound
+	msg["message"] = fmt.Sprintf("BandwidthProfile with ID %s not found.", id)
+	_ = json.NewEncoder(w).Encode(msg)
+
+	logger.Warnw(ctx, "sadis-bandwidthprofile-not-found", log.Fields{"id": id})
+}
diff --git a/internal/core/store.go b/internal/core/store.go
new file mode 100644
index 0000000..a6d24fb
--- /dev/null
+++ b/internal/core/store.go
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+
+ * http://www.apache.org/licenses/LICENSE-2.0
+
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package core
+
+import (
+	"context"
+	"fmt"
+	"github.com/opencord/voltha-lib-go/v4/pkg/log"
+	"sync"
+)
+
+type Store struct {
+	olts sync.Map
+	onus sync.Map
+	bps  sync.Map
+}
+
+func NewStore() *Store {
+	return &Store{
+		olts: sync.Map{},
+		onus: sync.Map{},
+		bps:  sync.Map{},
+	}
+}
+
+func (s *Store) addOlt(ctx context.Context, entry SadisOltEntry) {
+	logger.Debugw(ctx, "adding-olt", log.Fields{"olt": entry})
+	s.olts.Store(entry.ID, entry)
+}
+
+func (s *Store) addOnu(ctx context.Context, entry SadisOnuEntryV2) {
+	logger.Debugw(ctx, "adding-onu", log.Fields{"onu": entry})
+	s.onus.Store(entry.ID, entry)
+}
+
+func (s *Store) addBp(ctx context.Context, entry SadisBWPEntry) {
+	logger.Debugw(ctx, "adding-bp", log.Fields{"bp": entry})
+	s.bps.Store(entry.ID, entry)
+}
+
+func (s *Store) getOlt(ctx context.Context, id string) (*SadisOltEntry, error) {
+	logger.Debugw(ctx, "getting-olt", log.Fields{"olt": id})
+	if entry, ok := s.olts.Load(id); ok {
+		e := entry.(SadisOltEntry)
+		return &e, nil
+	}
+	return nil, fmt.Errorf("olt-not-found-in-store")
+}
+
+func (s *Store) getOnu(ctx context.Context, id string) (*SadisOnuEntryV2, error) {
+	logger.Debugw(ctx, "getting-onu", log.Fields{"onu": id})
+	if entry, ok := s.onus.Load(id); ok {
+		e := entry.(SadisOnuEntryV2)
+		return &e, nil
+	}
+	return nil, fmt.Errorf("onu-not-found-in-store")
+}
+
+func (s *Store) getBp(ctx context.Context, id string) (*SadisBWPEntry, error) {
+	logger.Debugw(ctx, "getting-bp", log.Fields{"bp": id})
+	if entry, ok := s.bps.Load(id); ok {
+		e := entry.(SadisBWPEntry)
+		return &e, nil
+	}
+	return nil, fmt.Errorf("bp-not-found-in-store")
+}
diff --git a/internal/core/watcher.go b/internal/core/watcher.go
new file mode 100644
index 0000000..99e75d4
--- /dev/null
+++ b/internal/core/watcher.go
@@ -0,0 +1,144 @@
+/*
+ * Copyright 2020-present Open Networking Foundation
+
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+
+ * http://www.apache.org/licenses/LICENSE-2.0
+
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package core
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+	"github.com/opencord/bbsim-sadis-server/internal/utils"
+	"github.com/opencord/voltha-lib-go/v4/pkg/log"
+	v1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/client-go/kubernetes"
+	"net/http"
+	"sync"
+	"time"
+)
+
+type Watcher struct {
+	client *kubernetes.Clientset
+	store  *Store
+	config *utils.ConfigFlags
+}
+
+func NewWatcher(client *kubernetes.Clientset, store *Store, cf *utils.ConfigFlags) *Watcher {
+	return &Watcher{
+		client: client,
+		store:  store,
+		config: cf,
+	}
+}
+
+func (w *Watcher) Watch(ctx context.Context, wg *sync.WaitGroup) {
+	defer wg.Done()
+	for {
+		logger.Debug(ctx, "fetching-pods")
+		services, err := w.client.CoreV1().Services("").List(context.TODO(), metav1.ListOptions{LabelSelector: "app=bbsim"})
+		if err != nil {
+			panic(err.Error())
+		}
+		logger.Debugf(ctx, "There are %d services in the cluster", len(services.Items))
+		w.handleServices(ctx, services)
+		time.Sleep(10 * time.Second)
+	}
+}
+
+func (w *Watcher) handleServices(ctx context.Context, services *v1.ServiceList) {
+	// TODO if a service is removed we'll want to remove the related entries
+	for _, service := range services.Items {
+		if err := w.queryService(ctx, service); err != nil {
+			logger.Errorw(ctx, "error-while-reading-from-service", log.Fields{"error": err.Error()})
+		}
+	}
+}
+
+func (w *Watcher) queryService(ctx context.Context, service v1.Service) error {
+	endpoint := fmt.Sprintf("%s.%s.svc:%d", service.Name, service.Namespace, w.config.BBsimSadisPort)
+	logger.Infow(ctx, "querying-service", log.Fields{"endpoint": endpoint})
+
+	res, err := http.Get(fmt.Sprintf("http://%s/v2/static", endpoint))
+
+	if err != nil {
+		return err
+	}
+
+	if res.Body != nil {
+		defer res.Body.Close()
+	}
+
+	var result SadisConfig
+
+	decoder := json.NewDecoder(res.Body)
+	if err := decoder.Decode(&result); err != nil {
+		logger.Errorw(ctx, "cannot-decode-sadis-response", log.Fields{"error": err.Error()})
+		return err
+	}
+
+	logger.Debugw(ctx, "fetched-sadis-config", log.Fields{
+		"endpoint":          endpoint,
+		"entries":           len(result.Sadis.Entries),
+		"bandwidthProfiles": len(result.BandwidthProfile.Entries),
+	})
+
+	//for _, entry := range result.Sadis.Entries {
+	//	switch entry.(type) {
+	//	case SadisOltEntry:
+	//	case *SadisOltEntry:
+	//		logger.Infow(ctx, "olt-entry", log.Fields{"entry": entry})
+	//	case SadisOnuEntryV2:
+	//	case *SadisOnuEntryV2:
+	//		logger.Infow(ctx, "onu-entry", log.Fields{"entry": entry})
+	//	default:
+	//		logger.Warnw(ctx, "unknown-entity", log.Fields{"entry": entry})
+	//	}
+	//}
+
+	for _, entry := range result.Sadis.Entries {
+		if entry.HardwareIdentifier != "" {
+			e := SadisOltEntry{
+				ID:                 entry.ID,
+				HardwareIdentifier: entry.HardwareIdentifier,
+				IPAddress:          entry.IPAddress,
+				NasID:              entry.NasID,
+				UplinkPort:         entry.UplinkPort,
+			}
+			w.store.addOlt(ctx, e)
+			continue
+		}
+		if len(entry.UniTagList) != 0 {
+			e := SadisOnuEntryV2{
+				ID:         entry.ID,
+				NasPortID:  entry.NasPortID,
+				CircuitID:  entry.CircuitID,
+				RemoteID:   entry.RemoteID,
+				UniTagList: entry.UniTagList,
+			}
+			w.store.addOnu(ctx, e)
+			continue
+		}
+		logger.Warnw(ctx, "unknown-entity", log.Fields{"entry": entry})
+	}
+
+	for _, bp := range result.BandwidthProfile.Entries {
+		w.store.addBp(ctx, *bp)
+	}
+
+	logger.Infow(ctx, "stored-sadis-config", log.Fields{"endpoint": endpoint})
+
+	return nil
+}