From a4973b5b5d89e28b9b8dfdbc2c8a6995bcaa88c8 Mon Sep 17 00:00:00 2001 From: Patricia Reinoso Date: Fri, 2 Aug 2024 05:45:32 +0200 Subject: [PATCH] feat: config gNBs and UPFs (#194) * dynamic parameter service Signed-off-by: Patricia Reinoso * fix linting Signed-off-by: Patricia Reinoso * add license to json example files Signed-off-by: Patricia Reinoso * use DB instead of files Signed-off-by: Patricia Reinoso * add ut Signed-off-by: Patricia Reinoso * add ut and fix format Signed-off-by: Patricia Reinoso * add more UT Signed-off-by: Patricia Reinoso * fix identation Signed-off-by: Patricia Reinoso * fix identation2 Signed-off-by: Patricia Reinoso * rename functions Signed-off-by: Patricia Reinoso --------- Signed-off-by: Patricia Reinoso --- configapi/api_inventory.go | 231 +++++++++++++++ configapi/api_inventory_test.go | 393 ++++++++++++++++++++++++++ configapi/routers.go | 36 +++ configmodels/config_msg.go | 5 + configmodels/model_inventory.go | 14 + proto/server/configEvtHandler.go | 65 ++++- proto/server/configEvtHandler_test.go | 68 +++++ 7 files changed, 811 insertions(+), 1 deletion(-) create mode 100644 configapi/api_inventory.go create mode 100644 configapi/api_inventory_test.go create mode 100644 configmodels/model_inventory.go diff --git a/configapi/api_inventory.go b/configapi/api_inventory.go new file mode 100644 index 00000000..2cd7d166 --- /dev/null +++ b/configapi/api_inventory.go @@ -0,0 +1,231 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2024 Canonical Ltd. + +package configapi + +import ( + "errors" + "fmt" + "encoding/json" + "strings" + "net/http" + + "github.com/gin-gonic/gin" + "github.com/omec-project/util/httpwrapper" + "github.com/omec-project/webconsole/backend/logger" + "github.com/omec-project/webconsole/configmodels" + "github.com/omec-project/webconsole/dbadapter" + "go.mongodb.org/mongo-driver/bson" +) + +const ( + gnbDataColl = "webconsoleData.snapshots.gnbData" + upfDataColl = "webconsoleData.snapshots.upfData" +) + +func setInventoryCorsHeader(c *gin.Context) { + c.Writer.Header().Set("Access-Control-Allow-Origin", "*") + c.Writer.Header().Set("Access-Control-Allow-Credentials", "true") + c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type") + c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, GET, DELETE") +} + +func GetGnbs(c *gin.Context) { + setInventoryCorsHeader(c) + logger.WebUILog.Infoln("Get all gNBs") + + var gnbs []*configmodels.Gnb + gnbs = make([]*configmodels.Gnb, 0) + rawGnbs, errGetMany := dbadapter.CommonDBClient.RestfulAPIGetMany(gnbDataColl, bson.M{}) + if errGetMany != nil { + logger.DbLog.Errorln(errGetMany) + c.JSON(http.StatusInternalServerError, gnbs) + } + + for _, rawGnb := range rawGnbs { + var gnbData configmodels.Gnb + err := json.Unmarshal(mapToByte(rawGnb), &gnbData) + if err != nil { + logger.DbLog.Errorf("Could not unmarshall gNB %v", rawGnb) + } + gnbs = append(gnbs, &gnbData) + } + c.JSON(http.StatusOK, gnbs) +} + +func PostGnb(c *gin.Context) { + setInventoryCorsHeader(c) + if err := handlePostGnb(c); err == nil { + c.JSON(http.StatusOK, gin.H{}) + } else { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + } +} + +func DeleteGnb(c *gin.Context) { + setInventoryCorsHeader(c) + if err := handleDeleteGnb(c); err == nil { + c.JSON(http.StatusOK, gin.H{}) + } else { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + } +} + +func handlePostGnb(c *gin.Context) error { + var gnbName string + var exists bool + if gnbName, exists = c.Params.Get("gnb-name"); !exists { + errorMessage := "Post gNB request is missing gnb-name" + configLog.Errorf(errorMessage) + return errors.New(errorMessage) + } + configLog.Infof("Received gNB %v", gnbName) + var err error + var newGnb configmodels.Gnb + + allowHeader := strings.Split(c.GetHeader("Content-Type"), ";") + switch allowHeader[0] { + case "application/json": + err = c.ShouldBindJSON(&newGnb) + } + if err != nil { + configLog.Errorf("err %v", err) + return fmt.Errorf("Failed to create gNB %v: %w", gnbName, err) + } + if newGnb.Tac == "" { + errorMessage := "Post gNB request body is missing tac" + configLog.Errorf(errorMessage) + return errors.New(errorMessage) + } + req := httpwrapper.NewRequest(c.Request, newGnb) + procReq := req.Body.(configmodels.Gnb) + procReq.Name = gnbName + msg := configmodels.ConfigMessage{ + MsgType: configmodels.Inventory, + MsgMethod: configmodels.Post_op, + GnbName: gnbName, + Gnb: &procReq, + } + configChannel <- &msg + configLog.Infof("Successfully added gNB [%v] to config channel.", gnbName) + return nil +} + +func handleDeleteGnb(c *gin.Context) error { + var gnbName string + var exists bool + if gnbName, exists = c.Params.Get("gnb-name"); !exists { + errorMessage := "Delete gNB request is missing gnb-name" + configLog.Errorf(errorMessage) + return fmt.Errorf(errorMessage) + } + configLog.Infof("Received delete gNB %v request", gnbName) + msg := configmodels.ConfigMessage{ + MsgType: configmodels.Inventory, + MsgMethod: configmodels.Delete_op, + GnbName: gnbName, + } + configChannel <- &msg + configLog.Infof("Successfully added gNB [%v] with delete_op to config channel.", gnbName) + return nil +} + +func GetUpfs(c *gin.Context) { + setInventoryCorsHeader(c) + logger.WebUILog.Infoln("Get all UPFs") + + var upfs []*configmodels.Upf + upfs = make([]*configmodels.Upf, 0) + rawUpfs, errGetMany := dbadapter.CommonDBClient.RestfulAPIGetMany(upfDataColl, bson.M{}) + if errGetMany != nil { + logger.DbLog.Errorln(errGetMany) + c.JSON(http.StatusInternalServerError, upfs) + } + + for _, rawUpf := range rawUpfs { + var upfData configmodels.Upf + err := json.Unmarshal(mapToByte(rawUpf), &upfData) + if err != nil { + logger.DbLog.Errorf("Could not unmarshall UPF %v", rawUpf) + } + upfs = append(upfs, &upfData) + } + c.JSON(http.StatusOK, upfs) +} + +func PostUpf(c *gin.Context) { + setInventoryCorsHeader(c) + if err := handlePostUpf(c); err == nil { + c.JSON(http.StatusOK, gin.H{}) + } else { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + } +} + +func DeleteUpf(c *gin.Context) { + setInventoryCorsHeader(c) + if err := handleDeleteUpf(c); err == nil { + c.JSON(http.StatusOK, gin.H{}) + } else { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + } +} + +func handlePostUpf(c *gin.Context) error { + var upfHostname string + var exists bool + if upfHostname, exists = c.Params.Get("upf-hostname"); !exists { + errorMessage := "Post UPF request is missing upf-hostname" + configLog.Errorf(errorMessage) + return errors.New(errorMessage) + } + configLog.Infof("Received UPF %v", upfHostname) + var err error + var newUpf configmodels.Upf + + allowHeader := strings.Split(c.GetHeader("Content-Type"), ";") + switch allowHeader[0] { + case "application/json": + err = c.ShouldBindJSON(&newUpf) + } + if err != nil { + configLog.Errorf("err %v", err) + return fmt.Errorf("Failed to create UPF %v: %w", upfHostname, err) + } + if newUpf.Port == "" { + errorMessage := "Post UPF request body is missing port" + configLog.Errorf(errorMessage) + return errors.New(errorMessage) + } + req := httpwrapper.NewRequest(c.Request, newUpf) + procReq := req.Body.(configmodels.Upf) + procReq.Hostname = upfHostname + msg := configmodels.ConfigMessage{ + MsgType: configmodels.Inventory, + MsgMethod: configmodels.Post_op, + UpfHostname: upfHostname, + Upf: &procReq, + } + configChannel <- &msg + configLog.Infof("Successfully added UPF [%v] to config channel.", upfHostname) + return nil +} + +func handleDeleteUpf(c *gin.Context) error { + var upfHostname string + var exists bool + if upfHostname, exists = c.Params.Get("upf-hostname"); !exists { + errorMessage := "Delete UPF request is missing upf-hostname" + configLog.Errorf(errorMessage) + return fmt.Errorf(errorMessage) + } + configLog.Infof("Received Delete UPF %v", upfHostname) + msg := configmodels.ConfigMessage{ + MsgType: configmodels.Inventory, + MsgMethod: configmodels.Delete_op, + UpfHostname: upfHostname, + } + configChannel <- &msg + configLog.Infof("Successfully added UPF [%v] with delete_op to config channel.", upfHostname) + return nil +} diff --git a/configapi/api_inventory_test.go b/configapi/api_inventory_test.go new file mode 100644 index 00000000..1464ed9c --- /dev/null +++ b/configapi/api_inventory_test.go @@ -0,0 +1,393 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2024 Canonical Ltd. + +package configapi + +import ( + "encoding/json" + "io" + "net/http/httptest" + "testing" + "strings" + "net/http" + + "github.com/gin-gonic/gin" + "github.com/omec-project/webconsole/configmodels" + "github.com/omec-project/webconsole/dbadapter" + "go.mongodb.org/mongo-driver/bson" +) + +type MockMongoClientNoGnbs struct { + dbadapter.DBInterface +} + +type MockMongoClientOneGnb struct { + dbadapter.DBInterface +} + +type MockMongoClientManyGnbs struct { + dbadapter.DBInterface +} + +type MockMongoClientNoUpfs struct { + dbadapter.DBInterface +} + +type MockMongoClientOneUpf struct { + dbadapter.DBInterface +} + +type MockMongoClientManyUpfs struct { + dbadapter.DBInterface +} + +func (m *MockMongoClientNoGnbs) RestfulAPIGetMany(coll string, filter bson.M) ([]map[string]interface{}, error) { + var results []map[string]interface{} + return results, nil +} + +func (m *MockMongoClientOneGnb) RestfulAPIGetMany(coll string, filter bson.M) ([]map[string]interface{}, error) { + var results []map[string]interface{} + gnb := configmodels.Gnb{ + Name: "gnb1", + Tac: "123", + } + var gnbBson bson.M + tmp, _ := json.Marshal(gnb) + json.Unmarshal(tmp, &gnbBson) + + results = append(results, gnbBson) + return results, nil +} + +var mockConfigChannel chan *configmodels.ConfigMessage + +func (m *MockMongoClientManyGnbs) RestfulAPIGetMany(coll string, filter bson.M) ([]map[string]interface{}, error) { + var results []map[string]interface{} + names := []string{"gnb0", "gnb1", "gnb2"} + tacs := []string{"12", "345", "678"} + for i, name := range names { + gnb := configmodels.Gnb{ + Name: name, + Tac: tacs[i], + } + var gnbBson bson.M + tmp, _ := json.Marshal(gnb) + json.Unmarshal(tmp, &gnbBson) + + results = append(results, gnbBson) + } + return results, nil +} + +func (m *MockMongoClientNoUpfs) RestfulAPIGetMany(coll string, filter bson.M) ([]map[string]interface{}, error) { + var results []map[string]interface{} + return results, nil +} + +func (m *MockMongoClientOneUpf) RestfulAPIGetMany(coll string, filter bson.M) ([]map[string]interface{}, error) { + var results []map[string]interface{} + upf := configmodels.Upf{ + Hostname: "upf1", + Port: "123", + } + var upfBson bson.M + tmp, _ := json.Marshal(upf) + json.Unmarshal(tmp, &upfBson) + + results = append(results, upfBson) + return results, nil +} + +func (m *MockMongoClientManyUpfs) RestfulAPIGetMany(coll string, filter bson.M) ([]map[string]interface{}, error) { + var results []map[string]interface{} + names := []string{"upf0", "upf1", "upf2"} + ports := []string{"12", "345", "678"} + for i, name := range names { + upf := configmodels.Upf{ + Hostname: name, + Port: ports[i], + } + var upfBson bson.M + tmp, _ := json.Marshal(upf) + json.Unmarshal(tmp, &upfBson) + + results = append(results, upfBson) + } + return results, nil +} + +func TestGivenNoGnbsWhenGetGnbsThenReturnsAnEmptyList(t *testing.T) { + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + dbadapter.CommonDBClient = &MockMongoClientNoGnbs{} + + GetGnbs(c) + + resp := w.Result() + if resp.StatusCode != 200 { + t.Errorf("Expected StatusCode %d, got %d", 200, resp.StatusCode) + } + body_bytes, _ := io.ReadAll(resp.Body) + body := string(body_bytes) + if body != "[]" { + t.Errorf("Expected empty JSON list, got %v", body) + } +} + +func TestGivenOneGnbWhenGetGnbsThenReturnsAListWithOneElement(t *testing.T) { + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + dbadapter.CommonDBClient = &MockMongoClientOneGnb{} + + GetGnbs(c) + + resp := w.Result() + if resp.StatusCode != 200 { + t.Errorf("Expected StatusCode %d, got %d", 200, resp.StatusCode) + } + body_bytes, _ := io.ReadAll(resp.Body) + body := string(body_bytes) + expected := `[{"name":"gnb1","tac":"123"}]` + if body != expected { + t.Errorf("Expected %v, got %v", expected, body) + } +} + +func TestGivenManyGnbsWhenGetGnbsThenReturnsAListWithManyGnbs(t *testing.T) { + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + dbadapter.CommonDBClient = &MockMongoClientManyGnbs{} + + GetGnbs(c) + + resp := w.Result() + if resp.StatusCode != 200 { + t.Errorf("Expected StatusCode %d, got %d", 200, resp.StatusCode) + } + body_bytes, _ := io.ReadAll(resp.Body) + body := string(body_bytes) + expected := `[{"name":"gnb0","tac":"12"},{"name":"gnb1","tac":"345"},{"name":"gnb2","tac":"678"}]` + if body != expected { + t.Errorf("Expected %v, got %v", expected, body) + } +} + + +func TestPostGnbByName(t *testing.T){ + gin.SetMode(gin.TestMode) + + t.Run("gNB TAC is not a string", func(t *testing.T) { + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + c.Params = gin.Params{{Key: "gnb-name", Value: "test-gnb"}} + req, _ := http.NewRequest(http.MethodPost, "/gnb", strings.NewReader(`{"tac": 1234}`)) + req.Header.Set("Content-Type", "application/json") + c.Request = req + + PostGnb(c) + + if w.Code != http.StatusBadRequest { + t.Errorf("Expected StatusCode %d, got %d", http.StatusBadRequest, w.Code) + } + expectedError := `{"error":"Failed to create gNB test-gnb: json: cannot unmarshal number into Go struct field Gnb.tac of type string"}` + if w.Body.String() != expectedError { + t.Errorf("Expected error %v, got %v", expectedError, w.Body.String()) + } + }) + + t.Run("Missing TAC in JSON body", func(t *testing.T) { + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + c.Params = gin.Params{{Key: "gnb-name", Value: "test-gnb"}} + req, _ := http.NewRequest(http.MethodPost, "/gnb", strings.NewReader(`{"some_param": "123"}`)) + req.Header.Set("Content-Type", "application/json") + c.Request = req + + PostGnb(c) + + if w.Code != http.StatusBadRequest { + t.Errorf("Expected StatusCode %d, got %d", http.StatusBadRequest, w.Code) + } + + expectedError := `{"error":"Post gNB request body is missing tac"}` + if w.Body.String() != expectedError { + t.Errorf("Expected error %v, got %v", expectedError, w.Body.String()) + } + }) + + t.Run("Missing gNB name", func(t *testing.T) { + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + req, _ := http.NewRequest(http.MethodPost, "/gnb", nil) + c.Request = req + + PostGnb(c) + + if w.Code != http.StatusBadRequest { + t.Errorf("Expected StatusCode %d, got %d", http.StatusBadRequest, w.Code) + } + expectedError := `{"error":"Post gNB request is missing gnb-name"}` + if w.Body.String() != expectedError { + t.Errorf("Expected error %v, got %v", expectedError, w.Body.String()) + } + }) +} + +func TestDeleteGnbByName(t *testing.T){ + gin.SetMode(gin.TestMode) + + t.Run("Missing gNB name", func(t *testing.T) { + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + req, _ := http.NewRequest(http.MethodPost, "/gnb", nil) + c.Request = req + + DeleteGnb(c) + + if w.Code != http.StatusBadRequest { + t.Errorf("Expected StatusCode %d, got %d", http.StatusBadRequest, w.Code) + } + expectedError := `{"error":"Delete gNB request is missing gnb-name"}` + if w.Body.String() != expectedError { + t.Errorf("Expected error %v, got %v", expectedError, w.Body.String()) + } + }) +} + +func TestGivenNoUpfsWhenGetUpfsThenReturnsAnEmptyList(t *testing.T) { + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + dbadapter.CommonDBClient = &MockMongoClientNoUpfs{} + + GetUpfs(c) + + resp := w.Result() + if resp.StatusCode != 200 { + t.Errorf("Expected StatusCode %d, got %d", 200, resp.StatusCode) + } + body_bytes, _ := io.ReadAll(resp.Body) + body := string(body_bytes) + if body != "[]" { + t.Errorf("Expected empty JSON list, got %v", body) + } +} + +func TestGivenOneUpfWhenGetUpfsThenReturnsAListWithOneUpf(t *testing.T) { + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + dbadapter.CommonDBClient = &MockMongoClientOneUpf{} + + GetUpfs(c) + + resp := w.Result() + if resp.StatusCode != 200 { + t.Errorf("Expected StatusCode %d, got %d", 200, resp.StatusCode) + } + body_bytes, _ := io.ReadAll(resp.Body) + body := string(body_bytes) + expected := `[{"hostname":"upf1","port":"123"}]` + if body != expected { + t.Errorf("Expected %v, got %v", expected, body) + } +} + +func TestGivenManyUpfsWhenGetUpfThenReturnsAListWithManyUpfs(t *testing.T) { + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + dbadapter.CommonDBClient = &MockMongoClientManyUpfs{} + + GetUpfs(c) + + resp := w.Result() + if resp.StatusCode != 200 { + t.Errorf("Expected StatusCode %d, got %d", 200, resp.StatusCode) + } + body_bytes, _ := io.ReadAll(resp.Body) + body := string(body_bytes) + expected := `[{"hostname":"upf0","port":"12"},{"hostname":"upf1","port":"345"},{"hostname":"upf2","port":"678"}]` + if body != expected { + t.Errorf("Expected %v, got %v", expected, body) + } +} + +func TestPostUpfByName(t *testing.T){ + gin.SetMode(gin.TestMode) + + t.Run("UPF port is not a string", func(t *testing.T) { + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + c.Params = gin.Params{{Key: "upf-hostname", Value: "test-upf"}} + req, _ := http.NewRequest(http.MethodPost, "/upf", strings.NewReader(`{"port": 1234}`)) + req.Header.Set("Content-Type", "application/json") + c.Request = req + + PostUpf(c) + + if w.Code != http.StatusBadRequest { + t.Errorf("Expected StatusCode %d, got %d", http.StatusBadRequest, w.Code) + } + expectedError := `{"error":"Failed to create UPF test-upf: json: cannot unmarshal number into Go struct field Upf.port of type string"}` + if w.Body.String() != expectedError { + t.Errorf("Expected error %v, got %v", expectedError, w.Body.String()) + } + }) + + t.Run("Missing port in JSON body", func(t *testing.T) { + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + c.Params = gin.Params{{Key: "upf-hostname", Value: "test-upf"}} + req, _ := http.NewRequest(http.MethodPost, "/upf", strings.NewReader(`{"some_param": "123"}`)) + req.Header.Set("Content-Type", "application/json") + c.Request = req + + PostUpf(c) + + if w.Code != http.StatusBadRequest { + t.Errorf("Expected StatusCode %d, got %d", http.StatusBadRequest, w.Code) + } + + expectedError := `{"error":"Post UPF request body is missing port"}` + if w.Body.String() != expectedError { + t.Errorf("Expected error %v, got %v", expectedError, w.Body.String()) + } + }) + + t.Run("Missing UPF hostname", func(t *testing.T) { + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + req, _ := http.NewRequest(http.MethodPost, "/upf", nil) + c.Request = req + + PostUpf(c) + + if w.Code != http.StatusBadRequest { + t.Errorf("Expected StatusCode %d, got %d", http.StatusBadRequest, w.Code) + } + expectedError := `{"error":"Post UPF request is missing upf-hostname"}` + if w.Body.String() != expectedError { + t.Errorf("Expected error %v, got %v", expectedError, w.Body.String()) + } + }) +} + +func TestDeleteUpfByName(t *testing.T){ + gin.SetMode(gin.TestMode) + + t.Run("Missing UPF hostname", func(t *testing.T) { + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + req, _ := http.NewRequest(http.MethodPost, "/upf", nil) + c.Request = req + + DeleteUpf(c) + + if w.Code != http.StatusBadRequest { + t.Errorf("Expected StatusCode %d, got %d", http.StatusBadRequest, w.Code) + } + expectedError := `{"error":"Delete UPF request is missing upf-hostname"}` + if w.Body.String() != expectedError { + t.Errorf("Expected error %v, got %v", expectedError, w.Body.String()) + } + }) +} diff --git a/configapi/routers.go b/configapi/routers.go index 13e8b567..594c9620 100644 --- a/configapi/routers.go +++ b/configapi/routers.go @@ -144,4 +144,40 @@ var routes = Routes{ "/network-slice/:slice-name", NetworkSliceSliceNamePut, }, + { + "GetGnbs", + http.MethodGet, + "/inventory/gnb", + GetGnbs, + }, + { + "PostGnb", + http.MethodPost, + "/inventory/gnb/:gnb-name", + PostGnb, + }, + { + "DeleteGnb", + http.MethodDelete, + "/inventory/gnb/:gnb-name", + DeleteGnb, + }, + { + "GetUpfs", + http.MethodGet, + "/inventory/upf", + GetUpfs, + }, + { + "PostUpf", + http.MethodPost, + "/inventory/upf/:upf-hostname", + PostUpf, + }, + { + "DeleteUpf", + http.MethodDelete, + "/inventory/upf/:upf-hostname", + DeleteUpf, + }, } diff --git a/configmodels/config_msg.go b/configmodels/config_msg.go index 1130aeaa..38023486 100644 --- a/configmodels/config_msg.go +++ b/configmodels/config_msg.go @@ -19,15 +19,20 @@ const ( Device_group = iota Network_slice Sub_data + Inventory ) type ConfigMessage struct { DevGroup *DeviceGroups Slice *Slice AuthSubData *models.AuthenticationSubscription + Gnb *Gnb + Upf *Upf DevGroupName string SliceName string Imsi string + GnbName string + UpfHostname string MsgType int MsgMethod int } diff --git a/configmodels/model_inventory.go b/configmodels/model_inventory.go new file mode 100644 index 00000000..1b51a590 --- /dev/null +++ b/configmodels/model_inventory.go @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2024 Canonical Ltd. + +package configmodels + +type Gnb struct { + Name string `json:"name"` + Tac string `json:"tac"` +} + +type Upf struct { + Hostname string `json:"hostname"` + Port string `json:"port"` +} diff --git a/proto/server/configEvtHandler.go b/proto/server/configEvtHandler.go index 2d07f5e3..a889b1e6 100644 --- a/proto/server/configEvtHandler.go +++ b/proto/server/configEvtHandler.go @@ -29,6 +29,8 @@ const ( flowRuleDataColl = "policyData.ues.flowRule" devGroupDataColl = "webconsoleData.snapshots.devGroupData" sliceDataColl = "webconsoleData.snapshots.sliceData" + gnbDataColl = "webconsoleData.snapshots.gnbData" + upfDataColl = "webconsoleData.snapshots.upfData" ) var configLog *logrus.Entry @@ -100,6 +102,16 @@ func configHandler(configMsgChan chan *configmodels.ConfigMessage, configReceive handleNetworkSlicePost(configMsg, subsUpdateChan) } + if configMsg.Gnb != nil { + configLog.Infof("Received gNB [%v] configuration from config channel", configMsg.GnbName) + handleGnbPost(configMsg) + } + + if configMsg.Upf != nil { + configLog.Infof("Received UPF [%v] configuration from config channel", configMsg.UpfHostname) + handleUpfPost(configMsg) + } + // loop through all clients and send this message to all clients if len(clientNFPool) == 0 { configLog.Infoln("No client available. No need to send config") @@ -110,7 +122,16 @@ func configHandler(configMsgChan chan *configmodels.ConfigMessage, configReceive } } else { var config5gMsg Update5GSubscriberMsg - if configMsg.MsgType != configmodels.Sub_data { + if configMsg.MsgType == configmodels.Inventory { + if configMsg.GnbName != "" { + configLog.Infof("Received delete gNB [%v] from config channel", configMsg.GnbName) + handleGnbDelete(configMsg) + } + if configMsg.UpfHostname != "" { + configLog.Infof("Received delete UPF [%v] from config channel", configMsg.UpfHostname) + handleUpfDelete(configMsg) + } + } else if configMsg.MsgType != configmodels.Sub_data { rwLock.Lock() // update config snapshot if configMsg.DevGroup == nil { @@ -200,6 +221,48 @@ func handleNetworkSlicePost(configMsg *configmodels.ConfigMessage, subsUpdateCha rwLock.Unlock() } +func handleGnbPost(configMsg *configmodels.ConfigMessage) { + rwLock.Lock() + filter := bson.M{"name": configMsg.GnbName} + gnbDataBson := toBsonM(configMsg.Gnb) + _, errPost := dbadapter.CommonDBClient.RestfulAPIPost(gnbDataColl, filter, gnbDataBson) + if errPost != nil { + logger.DbLog.Warnln(errPost) + } + rwLock.Unlock() +} + +func handleGnbDelete(configMsg *configmodels.ConfigMessage) { + rwLock.Lock() + filter := bson.M{"name": configMsg.GnbName} + errDelOne := dbadapter.CommonDBClient.RestfulAPIDeleteOne(gnbDataColl, filter) + if errDelOne != nil { + logger.DbLog.Warnln(errDelOne) + } + rwLock.Unlock() +} + +func handleUpfPost(configMsg *configmodels.ConfigMessage) { + rwLock.Lock() + filter := bson.M{"hostname": configMsg.UpfHostname} + upfDataBson := toBsonM(configMsg.Upf) + _, errPost := dbadapter.CommonDBClient.RestfulAPIPost(upfDataColl, filter, upfDataBson) + if errPost != nil { + logger.DbLog.Warnln(errPost) + } + rwLock.Unlock() +} + +func handleUpfDelete(configMsg *configmodels.ConfigMessage) { + rwLock.Lock() + filter := bson.M{"hostname": configMsg.UpfHostname} + errDelOne := dbadapter.CommonDBClient.RestfulAPIDeleteOne(upfDataColl, filter) + if errDelOne != nil { + logger.DbLog.Warnln(errDelOne) + } + rwLock.Unlock() +} + func firstConfigReceived() bool { return len(getDeviceGroups()) > 0 || len(getSlices()) > 0 } diff --git a/proto/server/configEvtHandler_test.go b/proto/server/configEvtHandler_test.go index ae1110ee..111db631 100644 --- a/proto/server/configEvtHandler_test.go +++ b/proto/server/configEvtHandler_test.go @@ -425,3 +425,71 @@ func Test_firstConfigReceived_sliceInDB(t *testing.T) { t.Errorf("Expected firstConfigReceived to return true, got %v", result) } } + +func TestPostGnb(t *testing.T) { + gnbName := "some-gnb" + newGnb := configmodels.Gnb{ + Name: gnbName, + Tac: "1233", + } + + configMsg := configmodels.ConfigMessage{ + MsgType: configmodels.Inventory, + MsgMethod: configmodels.Post_op, + GnbName: gnbName, + Gnb: &newGnb, + } + + postData = make([]map[string]interface{}, 0) + dbadapter.CommonDBClient = &MockMongoPost{} + handleGnbPost(&configMsg) + + expected_collection := "webconsoleData.snapshots.gnbData" + if postData[0]["coll"] != expected_collection { + t.Errorf("Expected collection %v, got %v", expected_collection, postData[0]["coll"]) + } + + expected_filter := bson.M{"name": gnbName} + if !reflect.DeepEqual(postData[0]["filter"], expected_filter) { + t.Errorf("Expected filter %v, got %v", expected_filter, postData[0]["filter"]) + } + + var result map[string]interface{} = postData[0]["data"].(map[string]interface{}) + if result["tac"] != newGnb.Tac { + t.Errorf("Expected port %v, got %v", newGnb.Tac, result["tac"]) + } +} + +func TestPostUpf(t *testing.T) { + upfHostname := "some-upf" + newUpf := configmodels.Upf{ + Hostname: upfHostname, + Port: "1233", + } + + configMsg := configmodels.ConfigMessage{ + MsgType: configmodels.Inventory, + MsgMethod: configmodels.Post_op, + UpfHostname: upfHostname, + Upf: &newUpf, + } + + postData = make([]map[string]interface{}, 0) + dbadapter.CommonDBClient = &MockMongoPost{} + handleUpfPost(&configMsg) + + expected_collection := "webconsoleData.snapshots.upfData" + if postData[0]["coll"] != expected_collection { + t.Errorf("Expected collection %v, got %v", expected_collection, postData[0]["coll"]) + } + + expected_filter := bson.M{"hostname": upfHostname} + if !reflect.DeepEqual(postData[0]["filter"], expected_filter) { + t.Errorf("Expected filter %v, got %v", expected_filter, postData[0]["filter"]) + } + + var result map[string]interface{} = postData[0]["data"].(map[string]interface{}) + if result["port"] != newUpf.Port { + t.Errorf("Expected port %v, got %v", newUpf.Port, result["port"]) + } +}