diff --git a/VERSION b/VERSION index ba562ef7..53adb84c 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.8.2-dev +1.8.2 diff --git a/backend/webui_service/webui_init.go b/backend/webui_service/webui_init.go index 403aa28c..6875eb10 100644 --- a/backend/webui_service/webui_init.go +++ b/backend/webui_service/webui_init.go @@ -165,7 +165,10 @@ func (webui *WEBUI) Start() { if !resp || err != nil { logger.InitLog.Errorf("error creating UPF index in commonDB %v", err) } - + resp, err = dbadapter.CommonDBClient.CreateIndex(configmodels.GnbDataColl, "name") + if !resp || err != nil { + logger.InitLog.Errorf("error creating gNB index in commonDB %v", err) + } logger.InitLog.Infoln("WebUI server started") /* First HTTP Server running at port to receive Config from ROC */ diff --git a/configapi/api_inventory.go b/configapi/api_inventory.go index 67c0fc84..0037269c 100644 --- a/configapi/api_inventory.go +++ b/configapi/api_inventory.go @@ -8,6 +8,8 @@ import ( "encoding/json" "fmt" "net/http" + "slices" + "strconv" "strings" "github.com/gin-gonic/gin" @@ -65,21 +67,124 @@ func GetGnbs(c *gin.Context) { // @Description Create a new gNB // @Tags gNBs // @Produce json -// @Param gnb-name path string true "Name of the gNB" -// @Param tac body configmodels.PostGnbRequest true "TAC of the gNB" +// @Param gnb body configmodels.PostGnbRequest true "Name and TAC of the gNB" // @Security BearerAuth -// @Success 200 {object} nil "gNB created" -// @Failure 400 {object} nil "Failed to create the gNB" +// @Success 201 {object} nil "gNB sucessfully created" +// @Failure 400 {object} nil "Bad request" // @Failure 401 {object} nil "Authorization failed" // @Failure 403 {object} nil "Forbidden" -// @Router /config/v1/inventory/gnb/{gnb-name} [post] +// @Failure 500 {object} nil "Error creating gNB" +// @Router /config/v1/inventory/gnb [post] 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()}) + logger.WebUILog.Infoln("received a POST gNB request") + var postGnbParams configmodels.PostGnbRequest + if err := c.ShouldBindJSON(&postGnbParams); err != nil { + logger.WebUILog.Errorw("invalid UPF gNB input parameters", "error", err) + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid JSON format"}) + return + } + if !isValidName(postGnbParams.Name) { + errorMessage := fmt.Sprintf("invalid gNB name '%s'. Name needs to match the following regular expression: %s", postGnbParams.Name, NAME_PATTERN) + logger.WebUILog.Errorln(errorMessage) + c.JSON(http.StatusBadRequest, gin.H{"error": errorMessage}) + return } + if !isValidGnbTac(postGnbParams.Tac) { + errorMessage := fmt.Sprintf("invalid gNB TAC '%v'. TAC must be a numeric string within the range [1, 16777215]", postGnbParams.Tac) + logger.WebUILog.Errorln(errorMessage) + c.JSON(http.StatusBadRequest, gin.H{"error": errorMessage}) + return + } + gnb := configmodels.Gnb(postGnbParams) + if err := executeGnbTransaction(c.Request.Context(), gnb, updateGnbInNetworkSlices, postGnbOperation); err != nil { + if strings.Contains(err.Error(), "E11000") { + logger.WebUILog.Errorw("duplicate gNB name found:", "error", err) + c.JSON(http.StatusBadRequest, gin.H{"error": "gNB already exists"}) + return + } + logger.WebUILog.Errorw("failed to create gNB", "name", postGnbParams.Name, "error", err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create gNB"}) + return + } + logger.WebUILog.Infof("successfully executed POST gNB %v request", postGnbParams.Name) + c.JSON(http.StatusCreated, gin.H{}) +} + +func postGnbOperation(sc mongo.SessionContext, gnb configmodels.Gnb) error { + filter := bson.M{"name": gnb.Name} + gnbDataBson := configmodels.ToBsonM(gnb) + return dbadapter.CommonDBClient.RestfulAPIPostManyWithContext(sc, configmodels.GnbDataColl, filter, []interface{}{gnbDataBson}) +} + +// PutGnb godoc +// +// @Description Create or update a gNB +// @Tags gNBs +// @Produce json +// @Param gnb-name path string true "Name of the gNB" +// @Param tac body configmodels.PutGnbRequest true "TAC of the gNB" +// @Security BearerAuth +// @Success 201 {object} nil "gNB sucessfully created" +// @Failure 400 {object} nil "Bad request" +// @Failure 401 {object} nil "Authorization failed" +// @Failure 403 {object} nil "Forbidden" +// @Failure 500 {object} nil "Error updating gNB" +// @Router /config/v1/inventory/gnb/{gnb-name} [put] +func PutGnb(c *gin.Context) { + setInventoryCorsHeader(c) + logger.WebUILog.Infoln("received a PUT gNB request") + gnbName, _ := c.Params.Get("gnb-name") + if !isValidName(gnbName) { + errorMessage := fmt.Sprintf("invalid gNB name '%s'. Name needs to match the following regular expression: %s", gnbName, NAME_PATTERN) + logger.WebUILog.Errorln(errorMessage) + c.JSON(http.StatusBadRequest, gin.H{"error": errorMessage}) + return + } + var putGnbParams configmodels.PutGnbRequest + if err := c.ShouldBindJSON(&putGnbParams); err != nil { + logger.WebUILog.Errorw("invalid gNB PUT input parameters", "name", gnbName, "error", err) + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid JSON format"}) + return + } + if !isValidGnbTac(putGnbParams.Tac) { + errorMessage := fmt.Sprintf("invalid gNB TAC '%v'. TAC must be a numeric string within the range [1, 16777215]", putGnbParams.Tac) + logger.WebUILog.Errorln(errorMessage) + c.JSON(http.StatusBadRequest, gin.H{"error": errorMessage}) + return + } + putGnb := configmodels.Gnb{ + Name: gnbName, + Tac: putGnbParams.Tac, + } + if err := executeGnbTransaction(c.Request.Context(), putGnb, updateGnbInNetworkSlices, putGnbOperation); err != nil { + logger.WebUILog.Errorw("failed to PUT gNB", "name", gnbName, "error", err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to PUT gNB"}) + return + } + logger.WebUILog.Infof("successfully executed PUT gNB request for hostname: %v", gnbName) + c.JSON(http.StatusOK, gin.H{}) +} + +func putGnbOperation(sc mongo.SessionContext, gnb configmodels.Gnb) error { + filter := bson.M{"name": gnb.Name} + gnbDataBson := configmodels.ToBsonM(gnb) + _, err := dbadapter.CommonDBClient.RestfulAPIPutOneWithContext(sc, configmodels.GnbDataColl, filter, gnbDataBson) + return err +} + +func updateGnbInNetworkSlices(gnb configmodels.Gnb) error { + filterByGnb := bson.M{ + "site-info.gNodeBs.name": gnb.Name, + } + tacNum, _ := strconv.ParseInt(gnb.Tac, 10, 32) + return updateInventoryInNetworkSlices(filterByGnb, func(networkSlice *configmodels.Slice) { + for i := range networkSlice.SiteInfo.GNodeBs { + if networkSlice.SiteInfo.GNodeBs[i].Name == gnb.Name { + networkSlice.SiteInfo.GNodeBs[i].Tac = int32(tacNum) + } + } + }) } // DeleteGnb godoc @@ -105,8 +210,10 @@ func DeleteGnb(c *gin.Context) { c.JSON(http.StatusBadRequest, gin.H{"error": errorMessage}) return } - filter := bson.M{"name": gnbName} - err := handleDeleteGnbTransaction(c.Request.Context(), filter, gnbName) + gnb := configmodels.Gnb{ + Name: gnbName, + } + err := executeGnbTransaction(c.Request.Context(), gnb, removeGnbFromNetworkSlices, deleteGnbOperation) if err != nil { logger.WebUILog.Errorw("failed to delete gNB", "gnbName", gnbName, "error", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to delete gNB"}) @@ -116,7 +223,23 @@ func DeleteGnb(c *gin.Context) { c.JSON(http.StatusOK, gin.H{}) } -func handleDeleteGnbTransaction(ctx context.Context, filter bson.M, gnbName string) error { +func deleteGnbOperation(sc mongo.SessionContext, gnb configmodels.Gnb) error { + filter := bson.M{"name": gnb.Name} + return dbadapter.CommonDBClient.RestfulAPIDeleteOneWithContext(sc, configmodels.GnbDataColl, filter) +} + +func removeGnbFromNetworkSlices(gnb configmodels.Gnb) error { + filterByGnb := bson.M{ + "site-info.gNodeBs.name": gnb.Name, + } + return updateInventoryInNetworkSlices(filterByGnb, func(networkSlice *configmodels.Slice) { + networkSlice.SiteInfo.GNodeBs = slices.DeleteFunc(networkSlice.SiteInfo.GNodeBs, func(existingGnb configmodels.SliceSiteInfoGNodeBs) bool { + return gnb.Name == existingGnb.Name + }) + }) +} + +func executeGnbTransaction(ctx context.Context, gnb configmodels.Gnb, nsOperation func(configmodels.Gnb) error, gnbOperation func(mongo.SessionContext, configmodels.Gnb) error) error { session, err := dbadapter.CommonDBClient.StartSession() if err != nil { return fmt.Errorf("failed to initialize DB session: %w", err) @@ -127,13 +250,14 @@ func handleDeleteGnbTransaction(ctx context.Context, filter bson.M, gnbName stri if err := session.StartTransaction(); err != nil { return fmt.Errorf("failed to start transaction: %w", err) } - if err = dbadapter.CommonDBClient.RestfulAPIDeleteOneWithContext(sc, configmodels.GnbDataColl, filter); err != nil { + if err := gnbOperation(sc, gnb); err != nil { if abortErr := session.AbortTransaction(sc); abortErr != nil { logger.DbLog.Errorw("failed to abort transaction", "error", abortErr) } - return fmt.Errorf("failed to delete gNB from collection: %w", err) + return err } - if err = updateGnbInNetworkSlices(gnbName, sc); err != nil { + err = nsOperation(gnb) + if err != nil { if abortErr := session.AbortTransaction(sc); abortErr != nil { logger.DbLog.Errorw("failed to abort transaction", "error", abortErr) } @@ -143,42 +267,6 @@ func handleDeleteGnbTransaction(ctx context.Context, filter bson.M, gnbName stri }) } -func handlePostGnb(c *gin.Context) error { - gnbName, _ := c.Params.Get("gnb-name") - if !isValidName(gnbName) { - errorMessage := fmt.Sprintf("invalid gNB name %s. Name needs to match the following regular expression: %s", gnbName, NAME_PATTERN) - logger.ConfigLog.Errorln(errorMessage) - return fmt.Errorf("%s", errorMessage) - } - logger.WebUILog.Infof("received a POST gNB %v request", gnbName) - if !strings.HasPrefix(c.GetHeader("Content-Type"), "application/json") { - return fmt.Errorf("invalid header") - } - var postGnbRequest configmodels.PostGnbRequest - err := c.ShouldBindJSON(&postGnbRequest) - if err != nil { - logger.WebUILog.Errorf("err %v", err) - return fmt.Errorf("invalid JSON format") - } - if postGnbRequest.Tac == "" { - errorMessage := "post gNB request body is missing tac" - logger.WebUILog.Errorln(errorMessage) - return fmt.Errorf("%s", errorMessage) - } - postGnb := configmodels.Gnb{ - Name: gnbName, - Tac: postGnbRequest.Tac, - } - msg := configmodels.ConfigMessage{ - MsgType: configmodels.Inventory, - MsgMethod: configmodels.Post_op, - Gnb: &postGnb, - } - configChannel <- &msg - logger.WebUILog.Infof("successfully added gNB [%v] to config channel", gnbName) - return nil -} - // GetUpfs godoc // // @Description Return the list of UPFs @@ -250,13 +338,7 @@ func PostUpf(c *gin.Context) { return } upf := configmodels.Upf(postUpfParams) - patchJSON, err := getEditUpfPatchJSON(upf) - if err != nil { - logger.WebUILog.Errorw("failed to serialize UPF", "hostname", upf.Hostname, "error", err) - c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to PUT UPF"}) - return - } - if err = executeUpfTransaction(c.Request.Context(), upf, patchJSON, postUpfOperation); err != nil { + if err = executeUpfTransaction(c.Request.Context(), upf, updateUpfInNetworkSlices, postUpfOperation); err != nil { if strings.Contains(err.Error(), "E11000") { logger.WebUILog.Errorw("duplicate hostname found:", "error", err) c.JSON(http.StatusBadRequest, gin.H{"error": "UPF already exists"}) @@ -270,6 +352,15 @@ func PostUpf(c *gin.Context) { c.JSON(http.StatusCreated, gin.H{}) } +func postUpfOperation(sc mongo.SessionContext, upf configmodels.Upf) error { + filter := bson.M{"hostname": upf.Hostname} + upfDataBson := configmodels.ToBsonM(upf) + if upfDataBson == nil { + return fmt.Errorf("failed to serialize UPF") + } + return dbadapter.CommonDBClient.RestfulAPIPostManyWithContext(sc, configmodels.UpfDataColl, filter, []interface{}{upfDataBson}) +} + // PutUpf godoc // // @Description Create or update a UPF @@ -311,32 +402,33 @@ func PutUpf(c *gin.Context) { Hostname: hostname, Port: putUpfParams.Port, } - patchJSON, err := getEditUpfPatchJSON(putUpf) - if err != nil { - logger.WebUILog.Errorw("failed to serialize UPF", "hostname", hostname, "error", err) - c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to PUT UPF"}) - return - } - if err := executeUpfTransaction(c.Request.Context(), putUpf, patchJSON, putUpfOperation); err != nil { + if err := executeUpfTransaction(c.Request.Context(), putUpf, updateUpfInNetworkSlices, putUpfOperation); err != nil { logger.WebUILog.Errorw("failed to PUT UPF", "hostname", hostname, "error", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to PUT UPF"}) return } + logger.WebUILog.Infof("successfully executed PUT UPF request for hostname: %v", hostname) c.JSON(http.StatusOK, gin.H{}) } -func getEditUpfPatchJSON(upf configmodels.Upf) ([]byte, error) { - patch := []dbadapter.PatchOperation{ - { - Op: "replace", - Path: "/site-info/upf", - Value: map[string]string{ - "upf-name": upf.Hostname, - "upf-port": upf.Port, - }, - }, - } - return json.Marshal(patch) +func putUpfOperation(sc mongo.SessionContext, upf configmodels.Upf) error { + filter := bson.M{"hostname": upf.Hostname} + upfDataBson := configmodels.ToBsonM(upf) + if upfDataBson == nil { + return fmt.Errorf("failed to serialize UPF") + } + _, err := dbadapter.CommonDBClient.RestfulAPIPutOneWithContext(sc, configmodels.UpfDataColl, filter, upfDataBson) + return err +} + +func updateUpfInNetworkSlices(upf configmodels.Upf) error { + filterByUpf := bson.M{"site-info.upf.upf-name": upf.Hostname} + return updateInventoryInNetworkSlices(filterByUpf, func(networkSlice *configmodels.Slice) { + networkSlice.SiteInfo.Upf = map[string]interface{}{ + "upf-name": upf.Hostname, + "upf-port": upf.Port, + } + }) } // DeleteUpf godoc @@ -365,14 +457,7 @@ func DeleteUpf(c *gin.Context) { upf := configmodels.Upf{ Hostname: hostname, } - patch := []dbadapter.PatchOperation{ - { - Op: "remove", - Path: "/site-info/upf", - }, - } - patchJSON, _ := json.Marshal(patch) - if err := executeUpfTransaction(c.Request.Context(), upf, patchJSON, deleteUpfOperation); err != nil { + if err := executeUpfTransaction(c.Request.Context(), upf, removeUpfFromNetworkSlices, deleteUpfOperation); err != nil { logger.WebUILog.Errorw("failed to delete UPF", "hostname", hostname, "error", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to delete UPF"}) return @@ -381,69 +466,19 @@ func DeleteUpf(c *gin.Context) { c.JSON(http.StatusOK, gin.H{}) } -func updateGnbInNetworkSlices(gnbName string, context context.Context) error { - filterByGnb := bson.M{ - "site-info.gNodeBs": bson.M{ - "$elemMatch": bson.M{"name": gnbName}, - }, - } - rawNetworkSlices, err := dbadapter.CommonDBClient.RestfulAPIGetMany(sliceDataColl, filterByGnb) - if err != nil { - return fmt.Errorf("failed to fetch network slices: %w", err) - } - for _, rawNetworkSlice := range rawNetworkSlices { - var networkSlice configmodels.Slice - if err = json.Unmarshal(configmodels.MapToByte(rawNetworkSlice), &networkSlice); err != nil { - return fmt.Errorf("error unmarshaling network slice: %v", err) - } - filteredGNodeBs := []configmodels.SliceSiteInfoGNodeBs{} - for _, gnb := range networkSlice.SiteInfo.GNodeBs { - if gnb.Name != gnbName { - filteredGNodeBs = append(filteredGNodeBs, gnb) - } - } - filteredGNodeBsJSON, err := json.Marshal(filteredGNodeBs) - if err != nil { - return fmt.Errorf("error marshaling GNodeBs: %v", err) - } - patchJSON := []byte( - fmt.Sprintf(`[{"op": "replace", "path": "/site-info/gNodeBs", "value": %s}]`, - string(filteredGNodeBsJSON)), - ) - filterBySliceName := bson.M{"slice-name": networkSlice.SliceName} - err = dbadapter.CommonDBClient.RestfulAPIJSONPatchWithContext(context, sliceDataColl, filterBySliceName, patchJSON) - if err != nil { - return err - } - } - return nil -} - -func postUpfOperation(sc mongo.SessionContext, upf configmodels.Upf) error { - filter := bson.M{"hostname": upf.Hostname} - upfDataBson := configmodels.ToBsonM(upf) - if upfDataBson == nil { - return fmt.Errorf("failed to serialize UPF") - } - return dbadapter.CommonDBClient.RestfulAPIPostManyWithContext(sc, configmodels.UpfDataColl, filter, []interface{}{upfDataBson}) -} - -func putUpfOperation(sc mongo.SessionContext, upf configmodels.Upf) error { - filter := bson.M{"hostname": upf.Hostname} - upfDataBson := configmodels.ToBsonM(upf) - if upfDataBson == nil { - return fmt.Errorf("failed to serialize UPF") - } - _, err := dbadapter.CommonDBClient.RestfulAPIPutOneWithContext(sc, configmodels.UpfDataColl, filter, upfDataBson) - return err -} - func deleteUpfOperation(sc mongo.SessionContext, upf configmodels.Upf) error { filter := bson.M{"hostname": upf.Hostname} return dbadapter.CommonDBClient.RestfulAPIDeleteOneWithContext(sc, configmodels.UpfDataColl, filter) } -func executeUpfTransaction(ctx context.Context, upf configmodels.Upf, patchJSON []byte, operation func(mongo.SessionContext, configmodels.Upf) error) error { +func removeUpfFromNetworkSlices(upf configmodels.Upf) error { + filterByUpf := bson.M{"site-info.upf.upf-name": upf.Hostname} + return updateInventoryInNetworkSlices(filterByUpf, func(networkSlice *configmodels.Slice) { + networkSlice.SiteInfo.Upf = nil + }) +} + +func executeUpfTransaction(ctx context.Context, upf configmodels.Upf, nsOperation func(configmodels.Upf) error, upfOperation func(mongo.SessionContext, configmodels.Upf) error) error { session, err := dbadapter.CommonDBClient.StartSession() if err != nil { return fmt.Errorf("failed to initialize DB session: %w", err) @@ -454,13 +489,13 @@ func executeUpfTransaction(ctx context.Context, upf configmodels.Upf, patchJSON if err := session.StartTransaction(); err != nil { return fmt.Errorf("failed to start transaction: %w", err) } - if err := operation(sc, upf); err != nil { + if err := upfOperation(sc, upf); err != nil { if abortErr := session.AbortTransaction(sc); abortErr != nil { logger.DbLog.Errorw("failed to abort transaction", "error", abortErr) } return err } - err = updateUpfInNetworkSlices(sc, upf.Hostname, patchJSON) + err = nsOperation(upf) if err != nil { if abortErr := session.AbortTransaction(sc); abortErr != nil { logger.DbLog.Errorw("failed to abort transaction", "error", abortErr) @@ -471,22 +506,32 @@ func executeUpfTransaction(ctx context.Context, upf configmodels.Upf, patchJSON }) } -func updateUpfInNetworkSlices(context context.Context, hostname string, patchJSON []byte) error { - filterByUpf := bson.M{"site-info.upf.upf-name": hostname} - rawNetworkSlices, err := dbadapter.CommonDBClient.RestfulAPIGetMany(sliceDataColl, filterByUpf) +func updateInventoryInNetworkSlices(filter bson.M, updateFunc func(*configmodels.Slice)) error { + rawNetworkSlices, err := dbadapter.CommonDBClient.RestfulAPIGetMany(sliceDataColl, filter) if err != nil { return fmt.Errorf("failed to fetch network slices: %w", err) } + + var messages []*configmodels.ConfigMessage for _, rawNetworkSlice := range rawNetworkSlices { - sliceName, ok := rawNetworkSlice["slice-name"].(string) - if !ok { - return fmt.Errorf("invalid slice-name in network slice: %v", rawNetworkSlice) + var networkSlice configmodels.Slice + if err = json.Unmarshal(configmodels.MapToByte(rawNetworkSlice), &networkSlice); err != nil { + return fmt.Errorf("error unmarshaling network slice: %v", err) } - filterBySliceName := bson.M{"slice-name": sliceName} - err = dbadapter.CommonDBClient.RestfulAPIJSONPatchWithContext(context, sliceDataColl, filterBySliceName, patchJSON) - if err != nil { - return err + + updateFunc(&networkSlice) + + msg := &configmodels.ConfigMessage{ + MsgMethod: configmodels.Post_op, + MsgType: configmodels.Network_slice, + Slice: &networkSlice, + SliceName: networkSlice.SliceName, } + messages = append(messages, msg) + } + for _, msg := range messages { + configChannel <- msg + logger.ConfigLog.Infof("network slice [%v] update sent to config channel", msg.SliceName) } return nil } diff --git a/configapi/api_inventory_test.go b/configapi/api_inventory_test.go index d8cdb60d..362f2291 100644 --- a/configapi/api_inventory_test.go +++ b/configapi/api_inventory_test.go @@ -212,7 +212,7 @@ func TestInventoryGetHandlers(t *testing.T) { } } -func TestGnbPostHandlers_Failure(t *testing.T) { +func TestGnbPostHandler(t *testing.T) { gin.SetMode(gin.TestMode) router := gin.Default() AddConfigV1Service(router) @@ -220,129 +220,183 @@ func TestGnbPostHandlers_Failure(t *testing.T) { testCases := []struct { name string route string + dbAdapter dbadapter.DBInterface inputData string - header string + expectedCode int expectedBody string }{ { - name: "TAC is not a string", - route: "/config/v1/inventory/gnb/gnb1", - inputData: `{"tac": 1234}`, - header: "application/json", + name: "Create a new gNB expects created status", + route: "/config/v1/inventory/gnb", + dbAdapter: &MockMongoClientEmptyDB{}, + inputData: `{"name": "gnb1", "tac": "123"}`, + expectedCode: http.StatusCreated, + expectedBody: "{}", + }, + { + name: "Create an existing gNB expects failure", + route: "/config/v1/inventory/gnb", + dbAdapter: &MockMongoClientDuplicateCreation{}, + inputData: `{"name": "gnb1", "tac": "123"}`, + expectedCode: http.StatusBadRequest, + expectedBody: `{"error":"gNB already exists"}`, + }, + { + name: "TAC is not a string expects failure", + route: "/config/v1/inventory/gnb", + dbAdapter: &MockMongoClientEmptyDB{}, + inputData: `{"name": "gnb1", "tac": 123}`, + expectedCode: http.StatusBadRequest, expectedBody: `{"error":"invalid JSON format"}`, }, { - name: "Missing TAC", - route: "/config/v1/inventory/gnb/gnb1", - inputData: `{"some_param": "123"}`, - header: "application/json", - expectedBody: `{"error":"post gNB request body is missing tac"}`, + name: "Missing TAC expects failure", + route: "/config/v1/inventory/gnb", + dbAdapter: &MockMongoClientEmptyDB{}, + inputData: `{"name": "gnb1"}`, + expectedCode: http.StatusBadRequest, + expectedBody: `{"error":"invalid gNB TAC ''. TAC must be a numeric string within the range [1, 16777215]"}`, }, { - name: "GnbInvalidHeader", - route: "/config/v1/inventory/gnb/gnb1", - inputData: `{"tac": "123"}`, - header: "application", - expectedBody: `{"error":"invalid header"}`, + name: "DB POST operation fails expects failure", + route: "/config/v1/inventory/gnb", + dbAdapter: &MockMongoClientDBError{}, + inputData: `{"name": "gnb1", "tac": "123"}`, + expectedCode: http.StatusInternalServerError, + expectedBody: `{"error":"failed to create gNB"}`, + }, + { + name: "TAC cannot be converted to int expects failure", + route: "/config/v1/inventory/gnb", + dbAdapter: &MockMongoClientEmptyDB{}, + inputData: `{"name": "gnb1", "tac": "a"}`, + expectedCode: http.StatusBadRequest, + expectedBody: `{"error":"invalid gNB TAC 'a'. TAC must be a numeric string within the range [1, 16777215]"}`, + }, + { + name: "gNB name not provided expects failure", + route: "/config/v1/inventory/gnb", + dbAdapter: &MockMongoClientEmptyDB{}, + inputData: `{"tac": "12"}`, + expectedCode: http.StatusBadRequest, + expectedBody: `{"error":"invalid gNB name ''. Name needs to match the following regular expression: ^[a-zA-Z0-9-_]+$"}`, + }, + { + name: "Invalid gNB name expects failure", + route: "/config/v1/inventory/gnb", + dbAdapter: &MockMongoClientEmptyDB{}, + inputData: `{"name": "gn!b1", "tac": "123"}`, + expectedCode: http.StatusBadRequest, + expectedBody: `{"error":"invalid gNB name 'gn!b1'. Name needs to match the following regular expression: ^[a-zA-Z0-9-_]+$"}`, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - origChannel := configChannel - configChannel = make(chan *configmodels.ConfigMessage, 1) - defer func() { configChannel = origChannel }() + dbadapter.CommonDBClient = tc.dbAdapter req, err := http.NewRequest(http.MethodPost, tc.route, strings.NewReader(tc.inputData)) if err != nil { t.Fatalf("failed to create request: %v", err) } - req.Header.Set("Content-Type", tc.header) w := httptest.NewRecorder() router.ServeHTTP(w, req) - expectedCode := http.StatusBadRequest - if expectedCode != w.Code { - t.Errorf("Expected `%v`, got `%v`", expectedCode, w.Code) + + if tc.expectedCode != w.Code { + t.Errorf("Expected `%v`, got `%v`", tc.expectedCode, w.Code) } - if w.Body.String() != tc.expectedBody { + if tc.expectedBody != w.Body.String() { t.Errorf("Expected `%v`, got `%v`", tc.expectedBody, w.Body.String()) } - select { - case msg := <-configChannel: - t.Errorf("unexpected message received: %+v", msg) - default: - // This is the expected outcome (no message received) - } }) } } -func TestGnbPostHandlers_Success(t *testing.T) { +func TestGnbPutHandler(t *testing.T) { gin.SetMode(gin.TestMode) router := gin.Default() AddConfigV1Service(router) testCases := []struct { - name string - route string - inputData string - expectedMessage configmodels.ConfigMessage + name string + route string + dbAdapter dbadapter.DBInterface + inputData string + expectedCode int + expectedBody string }{ { - name: "PostGnb", - route: "/config/v1/inventory/gnb/gnb1", - inputData: `{"tac": "123"}`, - expectedMessage: configmodels.ConfigMessage{ - MsgType: configmodels.Inventory, - MsgMethod: configmodels.Post_op, - Gnb: &configmodels.Gnb{ - Name: "gnb1", - Tac: "123", - }, - }, + name: "Put a new gNB expects OK status", + route: "/config/v1/inventory/gnb/gnb1", + dbAdapter: &MockMongoClientEmptyDB{}, + inputData: `{"tac": "123"}`, + expectedCode: http.StatusOK, + expectedBody: "{}", + }, + { + name: "Put an existing gNB expects a OK status", + route: "/config/v1/inventory/gnb/gnb1", + dbAdapter: &MockMongoClientPutExistingUpf{}, + inputData: `{"tac": "123"}`, + expectedCode: http.StatusOK, + expectedBody: "{}", + }, + { + name: "TAC is not a string expects failure", + route: "/config/v1/inventory/gnb/gnb1", + dbAdapter: &MockMongoClientEmptyDB{}, + inputData: `{"tac": 123}`, + expectedCode: http.StatusBadRequest, + expectedBody: `{"error":"invalid JSON format"}`, + }, + { + name: "Missing TAC expects failure", + route: "/config/v1/inventory/gnb/gnb1", + dbAdapter: &MockMongoClientEmptyDB{}, + inputData: `{"some_param": "123"}`, + expectedCode: http.StatusBadRequest, + expectedBody: `{"error":"invalid gNB TAC ''. TAC must be a numeric string within the range [1, 16777215]"}`, + }, + { + name: "DB PUT operation fails expects failure", + route: "/config/v1/inventory/gnb/gnb1", + dbAdapter: &MockMongoClientDBError{}, + inputData: `{"tac": "123"}`, + expectedCode: http.StatusInternalServerError, + expectedBody: `{"error":"failed to PUT gNB"}`, + }, + { + name: "TAC cannot be converted to int expects failure", + route: "/config/v1/inventory/gnb/gnb1", + dbAdapter: &MockMongoClientEmptyDB{}, + inputData: `{"tac": "a"}`, + expectedCode: http.StatusBadRequest, + expectedBody: `{"error":"invalid gNB TAC 'a'. TAC must be a numeric string within the range [1, 16777215]"}`, + }, + { + name: "Invalid gNB name expects failure", + route: "/config/v1/inventory/gnb/gn!b1", + dbAdapter: &MockMongoClientEmptyDB{}, + inputData: `{"tac": "123"}`, + expectedCode: http.StatusBadRequest, + expectedBody: `{"error":"invalid gNB name 'gn!b1'. Name needs to match the following regular expression: ^[a-zA-Z0-9-_]+$"}`, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - origChannel := configChannel - configChannel = make(chan *configmodels.ConfigMessage, 1) - defer func() { configChannel = origChannel }() - req, err := http.NewRequest(http.MethodPost, tc.route, strings.NewReader(tc.inputData)) + dbadapter.CommonDBClient = tc.dbAdapter + req, err := http.NewRequest(http.MethodPut, tc.route, strings.NewReader(tc.inputData)) if err != nil { t.Fatalf("failed to create request: %v", err) } - req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() router.ServeHTTP(w, req) - expectedCode := http.StatusOK - expectedBody := "{}" - - if expectedCode != w.Code { - t.Errorf("Expected `%v`, got `%v`", expectedCode, w.Code) - } - if w.Body.String() != expectedBody { - t.Errorf("Expected `%v`, got `%v`", expectedBody, w.Body.String()) + if tc.expectedCode != w.Code { + t.Errorf("Expected `%v`, got `%v`", tc.expectedCode, w.Code) } - select { - case msg := <-configChannel: - - if msg.MsgType != tc.expectedMessage.MsgType { - t.Errorf("expected MsgType %+v, but got %+v", tc.expectedMessage.MsgType, msg.MsgType) - } - if msg.MsgMethod != tc.expectedMessage.MsgMethod { - t.Errorf("expected MsgMethod %+v, but got %+v", tc.expectedMessage.MsgMethod, msg.MsgMethod) - } - if tc.expectedMessage.Gnb != nil { - if msg.Gnb == nil { - t.Errorf("expected gNB %+v, but got nil", tc.expectedMessage.Gnb) - } - if tc.expectedMessage.Gnb.Name != msg.Gnb.Name || tc.expectedMessage.Gnb.Tac != msg.Gnb.Tac { - t.Errorf("expected gNB %+v, but got %+v", tc.expectedMessage.Gnb, msg.Gnb) - } - } - default: - t.Error("expected message in configChannel, but none received") + if tc.expectedBody != w.Body.String() { + t.Errorf("Expected `%v`, got `%v`", tc.expectedBody, w.Body.String()) } }) } diff --git a/configapi/routers.go b/configapi/routers.go index 587f73a1..28c2f1a5 100644 --- a/configapi/routers.go +++ b/configapi/routers.go @@ -162,9 +162,15 @@ var routes = Routes{ { "PostGnb", http.MethodPost, - "/inventory/gnb/:gnb-name", + "/inventory/gnb", PostGnb, }, + { + "PutGnb", + http.MethodPut, + "/inventory/gnb/:gnb-name", + PutGnb, + }, { "DeleteGnb", http.MethodDelete, diff --git a/configapi/validators.go b/configapi/validators.go index 29bdf8e4..b232e24e 100644 --- a/configapi/validators.go +++ b/configapi/validators.go @@ -34,3 +34,11 @@ func isValidUpfPort(port string) bool { } return portNum >= 0 && portNum <= 65535 } + +func isValidGnbTac(tac string) bool { + tacNum, err := strconv.Atoi(tac) + if err != nil { + return false + } + return tacNum >= 1 && tacNum <= 16777215 +} diff --git a/configapi/validators_test.go b/configapi/validators_test.go index 067c55d4..fab19762 100644 --- a/configapi/validators_test.go +++ b/configapi/validators_test.go @@ -74,3 +74,27 @@ func TestValidateUpfPort(t *testing.T) { } } } + +func TestValidateGnbTac(t *testing.T) { + var testCases = []struct { + tac string + expected bool + }{ + {"123", true}, + {"7000", true}, + {"1", true}, + {"16777215", true}, + {"0", false}, + {"16777216", false}, + {"invalid", false}, + {"123ad", false}, + {"", false}, + } + + for _, tc := range testCases { + r := isValidGnbTac(tc.tac) + if r != tc.expected { + t.Errorf("%s", tc.tac) + } + } +} diff --git a/configmodels/config_msg.go b/configmodels/config_msg.go index 1c2f9eca..1130aeaa 100644 --- a/configmodels/config_msg.go +++ b/configmodels/config_msg.go @@ -19,14 +19,12 @@ const ( Device_group = iota Network_slice Sub_data - Inventory ) type ConfigMessage struct { DevGroup *DeviceGroups Slice *Slice AuthSubData *models.AuthenticationSubscription - Gnb *Gnb DevGroupName string SliceName string Imsi string diff --git a/configmodels/model_inventory.go b/configmodels/model_inventory.go index 44ada97e..51f39258 100644 --- a/configmodels/model_inventory.go +++ b/configmodels/model_inventory.go @@ -14,6 +14,11 @@ type Gnb struct { } type PostGnbRequest struct { + Name string `json:"name"` + Tac string `json:"tac"` +} + +type PutGnbRequest struct { Tac string `json:"tac"` } diff --git a/docs/docs.go b/docs/docs.go index 8178cb96..8cbb73b1 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -602,16 +602,58 @@ const docTemplate = `{ "description": "Error retrieving gNBs" } } + }, + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Create a new gNB", + "produces": [ + "application/json" + ], + "tags": [ + "gNBs" + ], + "parameters": [ + { + "description": "Name and TAC of the gNB", + "name": "gnb", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/configmodels.PostGnbRequest" + } + } + ], + "responses": { + "201": { + "description": "gNB sucessfully created" + }, + "400": { + "description": "Bad request" + }, + "401": { + "description": "Authorization failed" + }, + "403": { + "description": "Forbidden" + }, + "500": { + "description": "Error creating gNB" + } + } } }, "/config/v1/inventory/gnb/{gnb-name}": { - "post": { + "put": { "security": [ { "BearerAuth": [] } ], - "description": "Create a new gNB", + "description": "Create or update a gNB", "produces": [ "application/json" ], @@ -632,22 +674,25 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/configmodels.PostGnbRequest" + "$ref": "#/definitions/configmodels.PutGnbRequest" } } ], "responses": { - "200": { - "description": "gNB created" + "201": { + "description": "gNB sucessfully created" }, "400": { - "description": "Failed to create the gNB" + "description": "Bad request" }, "401": { "description": "Authorization failed" }, "403": { "description": "Forbidden" + }, + "500": { + "description": "Error updating gNB" } } }, @@ -1235,6 +1280,9 @@ const docTemplate = `{ "configmodels.PostGnbRequest": { "type": "object", "properties": { + "name": { + "type": "string" + }, "tac": { "type": "string" } @@ -1251,6 +1299,14 @@ const docTemplate = `{ } } }, + "configmodels.PutGnbRequest": { + "type": "object", + "properties": { + "tac": { + "type": "string" + } + } + }, "configmodels.PutUpfRequest": { "type": "object", "properties": { diff --git a/proto/server/configEvtHandler.go b/proto/server/configEvtHandler.go index 3fb4c4bc..4e1860dc 100644 --- a/proto/server/configEvtHandler.go +++ b/proto/server/configEvtHandler.go @@ -30,8 +30,6 @@ const ( flowRuleDataColl = "policyData.ues.flowRule" devGroupDataColl = "webconsoleData.snapshots.devGroupData" sliceDataColl = "webconsoleData.snapshots.sliceData" - gnbDataColl = "webconsoleData.snapshots.gnbData" - upfDataColl = "webconsoleData.snapshots.upfData" ) type Update5GSubscriberMsg struct { @@ -97,10 +95,6 @@ func configHandler(configMsgChan chan *configmodels.ConfigMessage, configReceive handleNetworkSlicePost(configMsg, subsUpdateChan) } - if configMsg.Gnb != nil { - logger.ConfigLog.Infof("received gNB [%v] configuration from config channel", configMsg.Gnb.Name) - handleGnbPost(configMsg.Gnb) - } // loop through all clients and send this message to all clients if len(clientNFPool) == 0 { logger.ConfigLog.Infoln("no client available. No need to send config") @@ -228,17 +222,6 @@ func handleNetworkSliceDelete(configMsg *configmodels.ConfigMessage, subsUpdateC rwLock.Unlock() } -func handleGnbPost(gnb *configmodels.Gnb) { - rwLock.Lock() - filter := bson.M{"name": gnb.Name} - gnbDataBson := configmodels.ToBsonM(gnb) - _, errPost := dbadapter.CommonDBClient.RestfulAPIPost(gnbDataColl, filter, gnbDataBson) - if errPost != nil { - logger.DbLog.Warnln(errPost) - } - 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 836fce43..1f970ab6 100644 --- a/proto/server/configEvtHandler_test.go +++ b/proto/server/configEvtHandler_test.go @@ -481,29 +481,3 @@ 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", - } - postData = make([]map[string]interface{}, 0) - dbadapter.CommonDBClient = &MockMongoPost{} - handleGnbPost(&newGnb) - - 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"]) - } -}