Skip to content

feat: delete UPF and gNB is propagated to NS #279

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ The authentication and authorization feature ensures that only verified and auth

This is an optional feature, disabled by default. For more details, refer to this [file](backend/auth/README.md).

## MongoDB Transaction Support

This application requires a MongoDB deployment configured to support transactions, such as a replica set or a sharded cluster. Standalone MongoDB instances do not support transactions and will prevent the application from starting. Please ensure your MongoDB instance is properly set up for transactions. For detailed configuration instructions, see the [MongoDB Replica Set Documentation](https://www.mongodb.com/docs/kubernetes-operator/current/tutorial/deploy-replica-set/).

## Webconsole Architecture diagram

![Architecture](/docs/images/architecture1.png)
Expand Down
4 changes: 4 additions & 0 deletions backend/webui_service/webui_init.go
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,10 @@ func (webui *WEBUI) Start() {
if factory.WebUIConfig.Configuration.Mode5G {
// Connect to MongoDB
dbadapter.ConnectMongo(mongodb.Url, mongodb.Name, &dbadapter.CommonDBClient)
if err := dbadapter.CheckTransactionsSupport(&dbadapter.CommonDBClient); err != nil {
logger.DbLog.Errorw("failed to connect to MongoDB client", mongodb.Name, "error", err)
return
}
dbadapter.ConnectMongo(mongodb.AuthUrl, mongodb.AuthKeysDbName, &dbadapter.AuthDBClient)
}

Expand Down
2 changes: 1 addition & 1 deletion configapi/api_default.go
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ func NetworkSliceSliceNameDelete(c *gin.Context) {
// @Failure 401 {object} nil "Authorization failed"
// @Failure 403 {object} nil "Forbidden"
// @Failure 500 {object} nil "Error creating network slice"
// @Router /config/v1/network-slice/{sliceName [post]
// @Router /config/v1/network-slice/{sliceName} [post]
func NetworkSliceSliceNamePost(c *gin.Context) {
logger.ConfigLog.Debugf("Received NetworkSliceSliceNamePost ")
if ret := NetworkSlicePostHandler(c, configmodels.Post_op); ret {
Expand Down
229 changes: 167 additions & 62 deletions configapi/api_inventory.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
package configapi

import (
"context"
"encoding/json"
"fmt"
"net/http"
Expand All @@ -14,6 +15,7 @@ import (
"github.com/omec-project/webconsole/configmodels"
"github.com/omec-project/webconsole/dbadapter"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo"
)

const (
Expand Down Expand Up @@ -41,25 +43,25 @@ func setInventoryCorsHeader(c *gin.Context) {
// @Router /config/v1/inventory/gnb [get]
func GetGnbs(c *gin.Context) {
setInventoryCorsHeader(c)
logger.WebUILog.Infoln("get all gNBs")

logger.WebUILog.Infoln("received a GET gNBs request")
var gnbs []*configmodels.Gnb
gnbs = make([]*configmodels.Gnb, 0)
rawGnbs, errGetMany := dbadapter.CommonDBClient.RestfulAPIGetMany(gnbDataColl, bson.M{})
if errGetMany != nil {
logger.DbLog.Errorln(errGetMany)
rawGnbs, err := dbadapter.CommonDBClient.RestfulAPIGetMany(gnbDataColl, bson.M{})
if err != nil {
logger.DbLog.Errorw("failed to retrieve gNBs", "error", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to retrieve gNBs"})
return
}

for _, rawGnb := range rawGnbs {
var gnbData configmodels.Gnb
err := json.Unmarshal(configmodels.MapToByte(rawGnb), &gnbData)
err = json.Unmarshal(configmodels.MapToByte(rawGnb), &gnbData)
if err != nil {
logger.DbLog.Errorf("could not unmarshal gNB %v", rawGnb)
}
gnbs = append(gnbs, &gnbData)
}
logger.WebUILog.Infoln("successfully executed GET gNBs request")
c.JSON(http.StatusOK, gnbs)
}

Expand Down Expand Up @@ -93,39 +95,79 @@ func PostGnb(c *gin.Context) {
// @Param gnb-name path string true "Name of the gNB"
// @Security BearerAuth
// @Success 200 {object} nil "gNB deleted"
// @Failure 400 {object} nil "Failed to delete the gNB"
// @Failure 400 {object} nil "Bad request"
// @Failure 401 {object} nil "Authorization failed"
// @Failure 403 {object} nil "Forbidden"
// @Failure 500 {object} nil "Failed to delete gNB"
// @Router /config/v1/inventory/gnb/{gnb-name} [delete]
func DeleteGnb(c *gin.Context) {
logger.WebUILog.Infoln("received a DELETE gNB request")
setInventoryCorsHeader(c)
if err := handleDeleteGnb(c); err == nil {
c.JSON(http.StatusOK, gin.H{})
} else {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
gnbName, exists := c.Params.Get("gnb-name")
if !exists {
errorMessage := "delete gNB request is missing path param `gnb-name`"
logger.WebUILog.Errorln(errorMessage)
c.JSON(http.StatusBadRequest, gin.H{"error": errorMessage})
return
}
filter := bson.M{"name": gnbName}
err := handleDeleteGnbTransaction(c.Request.Context(), filter, gnbName)
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"})
return
}
logger.WebUILog.Infof("successfully executed DELETE gNB %v request", gnbName)
c.JSON(http.StatusOK, gin.H{})
}

func handleDeleteGnbTransaction(ctx context.Context, filter bson.M, gnbName string) error {
session, err := dbadapter.CommonDBClient.StartSession()
if err != nil {
return fmt.Errorf("failed to initialize DB session: %w", err)
}
defer session.EndSession(ctx)

return mongo.WithSession(ctx, session, func(sc mongo.SessionContext) error {
if err := session.StartTransaction(); err != nil {
return fmt.Errorf("failed to start transaction: %w", err)
}
if err = dbadapter.CommonDBClient.RestfulAPIDeleteOneWithContext(gnbDataColl, filter, sc); 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)
}
if err = updateGnbInNetworkSlices(gnbName, sc); err != nil {
if abortErr := session.AbortTransaction(sc); abortErr != nil {
logger.DbLog.Errorw("failed to abort transaction", "error", abortErr)
}
return fmt.Errorf("failed to update network slices: %w", err)
}
return session.CommitTransaction(sc)
})
}

func handlePostGnb(c *gin.Context) error {
gnbName, exists := c.Params.Get("gnb-name")
if !exists {
errorMessage := "post gNB request is missing gnb-name"
logger.ConfigLog.Errorln(errorMessage)
logger.WebUILog.Errorln(errorMessage)
return fmt.Errorf("%s", errorMessage)
}
logger.ConfigLog.Infof("received gNB %v", gnbName)
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.ConfigLog.Errorf("err %v", err)
logger.WebUILog.Errorf("err %v", err)
return fmt.Errorf("invalid JSON format")
}
if postGnbRequest.Tac == "" {
errorMessage := "post gNB request body is missing tac"
logger.ConfigLog.Errorln(errorMessage)
logger.WebUILog.Errorln(errorMessage)
return fmt.Errorf("%s", errorMessage)
}
postGnb := configmodels.Gnb{
Expand All @@ -138,25 +180,7 @@ func handlePostGnb(c *gin.Context) error {
Gnb: &postGnb,
}
configChannel <- &msg
logger.ConfigLog.Infof("successfully added gNB [%v] to config channel", gnbName)
return nil
}

func handleDeleteGnb(c *gin.Context) error {
gnbName, exists := c.Params.Get("gnb-name")
if !exists {
errorMessage := "delete gNB request is missing gnb-name"
logger.ConfigLog.Errorln(errorMessage)
return fmt.Errorf("%s", errorMessage)
}
logger.ConfigLog.Infof("received delete gNB %v request", gnbName)
msg := configmodels.ConfigMessage{
MsgType: configmodels.Inventory,
MsgMethod: configmodels.Delete_op,
GnbName: gnbName,
}
configChannel <- &msg
logger.ConfigLog.Infof("successfully added gNB [%v] with delete_op to config channel", gnbName)
logger.WebUILog.Infof("successfully added gNB [%v] to config channel", gnbName)
return nil
}

Expand All @@ -173,13 +197,12 @@ func handleDeleteGnb(c *gin.Context) error {
// @Router /config/v1/inventory/upf [get]
func GetUpfs(c *gin.Context) {
setInventoryCorsHeader(c)
logger.WebUILog.Infoln("get all UPFs")

logger.WebUILog.Infoln("received a GET UPFs request")
var upfs []*configmodels.Upf
upfs = make([]*configmodels.Upf, 0)
rawUpfs, errGetMany := dbadapter.CommonDBClient.RestfulAPIGetMany(upfDataColl, bson.M{})
if errGetMany != nil {
logger.DbLog.Errorln(errGetMany)
rawUpfs, err := dbadapter.CommonDBClient.RestfulAPIGetMany(upfDataColl, bson.M{})
if err != nil {
logger.DbLog.Errorw("failed to retrieve UPFs", "error", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to retrieve UPFs"})
return
}
Expand All @@ -192,6 +215,7 @@ func GetUpfs(c *gin.Context) {
}
upfs = append(upfs, &upfData)
}
logger.WebUILog.Infoln("successfully executed GET UPFs request")
c.JSON(http.StatusOK, upfs)
}

Expand Down Expand Up @@ -225,39 +249,80 @@ func PostUpf(c *gin.Context) {
// @Param upf-hostname path string true "Name of the UPF"
// @Security BearerAuth
// @Success 200 {object} nil "UPF deleted"
// @Failure 400 {object} nil "Failed to delete the UPF"
// @Failure 400 {object} nil "Bad request"
// @Failure 401 {object} nil "Authorization failed"
// @Failure 403 {object} nil "Forbidden"
// @Failure 500 {object} nil "Failed to delete UPF"
// @Router /config/v1/inventory/upf/{upf-hostname} [delete]
func DeleteUpf(c *gin.Context) {
logger.WebUILog.Infoln("received a DELETE UPF request")
setInventoryCorsHeader(c)
if err := handleDeleteUpf(c); err == nil {
c.JSON(http.StatusOK, gin.H{})
} else {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
hostname, exists := c.Params.Get("upf-hostname")
if !exists {
errorMessage := "delete gNB request is missing path param `upf-hostname`"
logger.WebUILog.Errorln(errorMessage)
c.JSON(http.StatusBadRequest, gin.H{"error": errorMessage})
return
}
filter := bson.M{"hostname": hostname}
err := handleDeleteUpfTransaction(c.Request.Context(), filter, hostname)
if 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
}
logger.WebUILog.Infof("successfully executed DELETE UPF request for hostname: %v", hostname)
c.JSON(http.StatusOK, gin.H{})
}

func handleDeleteUpfTransaction(ctx context.Context, filter bson.M, hostname string) error {
session, err := dbadapter.CommonDBClient.StartSession()
if err != nil {
return fmt.Errorf("failed to initialize DB session: %w", err)
}
defer session.EndSession(ctx)

return mongo.WithSession(ctx, session, func(sc mongo.SessionContext) error {
if err := session.StartTransaction(); err != nil {
return fmt.Errorf("failed to start transaction: %w", err)
}
if err = dbadapter.CommonDBClient.RestfulAPIDeleteOneWithContext(upfDataColl, filter, sc); err != nil {
if abortErr := session.AbortTransaction(sc); abortErr != nil {
logger.DbLog.Errorw("failed to abort transaction", "error", abortErr)
}
return err
}
patchJSON := []byte(`[{"op": "remove", "path": "/site-info/upf"}]`)
if err = updateUpfInNetworkSlices(hostname, patchJSON, sc); err != nil {
if abortErr := session.AbortTransaction(sc); abortErr != nil {
logger.DbLog.Errorw("failed to abort transaction", "error", abortErr)
}
return fmt.Errorf("failed to update network slices: %w", err)
}
return session.CommitTransaction(sc)
})
}

func handlePostUpf(c *gin.Context) error {
upfHostname, exists := c.Params.Get("upf-hostname")
if !exists {
errorMessage := "post UPF request is missing upf-hostname"
logger.ConfigLog.Errorln(errorMessage)
logger.WebUILog.Errorln(errorMessage)
return fmt.Errorf("%s", errorMessage)
}
logger.ConfigLog.Infof("received UPF %v", upfHostname)
logger.WebUILog.Infof("received a POST UPF %v request", upfHostname)
if !strings.HasPrefix(c.GetHeader("Content-Type"), "application/json") {
return fmt.Errorf("invalid header")
}
var postUpfRequest configmodels.PostUpfRequest
err := c.ShouldBindJSON(&postUpfRequest)
if err != nil {
logger.ConfigLog.Errorf("err %v", err)
logger.WebUILog.Errorf("err %v", err)
return fmt.Errorf("invalid JSON format")
}
if postUpfRequest.Port == "" {
errorMessage := "post UPF request body is missing port"
logger.ConfigLog.Errorln(errorMessage)
logger.WebUILog.Errorln(errorMessage)
return fmt.Errorf("%s", errorMessage)
}
postUpf := configmodels.Upf{
Expand All @@ -270,24 +335,64 @@ func handlePostUpf(c *gin.Context) error {
Upf: &postUpf,
}
configChannel <- &msg
logger.ConfigLog.Infof("successfully added UPF [%v] to config channel", upfHostname)
logger.WebUILog.Infof("successfully added UPF [%v] to config channel", upfHostname)
return nil
}

func handleDeleteUpf(c *gin.Context) error {
upfHostname, exists := c.Params.Get("upf-hostname")
if !exists {
errorMessage := "delete UPF request is missing upf-hostname"
logger.ConfigLog.Errorln(errorMessage)
return fmt.Errorf("%s", errorMessage)
func updateGnbInNetworkSlices(gnbName string, context context.Context) error {
filterByGnb := bson.M{
"site-info.gNodeBs": bson.M{
"$elemMatch": bson.M{"name": gnbName},
},
}
logger.ConfigLog.Infof("received delete UPF %v", upfHostname)
msg := configmodels.ConfigMessage{
MsgType: configmodels.Inventory,
MsgMethod: configmodels.Delete_op,
UpfHostname: upfHostname,
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(sliceDataColl, filterBySliceName, patchJSON, context)
if err != nil {
return err
}
}
return nil
}

func updateUpfInNetworkSlices(hostname string, patchJSON []byte, context context.Context) error {
filterByUpf := bson.M{"site-info.upf.upf-name": hostname}
rawNetworkSlices, err := dbadapter.CommonDBClient.RestfulAPIGetMany(sliceDataColl, filterByUpf)
if err != nil {
return fmt.Errorf("failed to fetch network slices: %w", err)
}
for _, rawNetworkSlice := range rawNetworkSlices {
sliceName, ok := rawNetworkSlice["slice-name"].(string)
if !ok {
return fmt.Errorf("invalid slice-name in network slice: %v", rawNetworkSlice)
}
filterBySliceName := bson.M{"slice-name": sliceName}
err = dbadapter.CommonDBClient.RestfulAPIJSONPatchWithContext(sliceDataColl, filterBySliceName, patchJSON, context)
if err != nil {
return err
}
}
configChannel <- &msg
logger.ConfigLog.Infof("successfully added UPF [%v] with delete_op to config channel", upfHostname)
return nil
}
Loading
Loading