diff --git a/CHANGELOG.md b/CHANGELOG.md index ea99f16a..464646bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,48 @@ # Changelog +## v1.6.0 + +***Summary:*** + +> - *This version focuses on specific enchancements for both AsSessionWithQoS and Monitoring Event APIs* +> - *AsSessionWithQoS API*: +> - *Provision of periodic reports, (NetApp indicates the reporting period in sec)* +> - *MonitoringEvent API*: +> - *Addition of LOSS_OF_CONNECTIVITY event, Network detects that the UE is no longer reachable for either signalling or user plane communication. The NetApp may provide a Maximum Detection Time, which indicates the maximum period of time without any communication with the UE (after the UE is considered to be unreachable by the network)* +> - *Addition of UE_REACHABILITY event, which indicates when the UE becomes reachable (for sending downlink data to the UE)* + + +## UI changes + + - 👉 replace common html blocks with reusable Jinja2 templates (header, sidebar, footer) + + +## Backend + + + - ➕ Addition of two events on `MonitoringEvent API` ➡`/api/v1/3gpp-monitoring-event/v1/{scsAsId}/subscriptions` 👇 + - `LOSS_OF_CONNECTIVITY` event + - `UE_REACHABILITY` event +- ➕ Addition of periodic reports for `AsSessionWithQoS API` ➡`/api/v1/3gpp-as-session-with-qos/v1/{scsAsId}/subscriptions` + + + +## Database + + - Optimization on MongoDB 👇 + - MongoClient instance from pymongo module is created once in `backend/app/app/db/session.py` + + + +## Other + + - ✔ `make logs-backend` : display the logs only in the backend service + - ✔ `make logs-mongo` : display the logs only for mongo service + + +

+ + ## v1.5.0 ***Summary:*** diff --git a/Makefile b/Makefile index 0c2ba80e..a592fdab 100644 --- a/Makefile +++ b/Makefile @@ -38,6 +38,12 @@ build-no-cache: logs: docker-compose logs -f +logs-backend: + docker-compose logs -f backend + +logs-mongo: + docker-compose logs -f mongo + ps: docker ps -a diff --git a/backend/app/app/api/api_v1/endpoints/monitoringevent.py b/backend/app/app/api/api_v1/endpoints/monitoringevent.py index d206941d..bd6001c1 100644 --- a/backend/app/app/api/api_v1/endpoints/monitoringevent.py +++ b/backend/app/app/api/api_v1/endpoints/monitoringevent.py @@ -8,6 +8,7 @@ from app.crud import crud_mongo, user, ue from app.api import deps from app import tools +from app.db.session import client from app.api.api_v1.endpoints.utils import add_notifications from .ue_movement import retrieve_ue_state, retrieve_ue @@ -18,13 +19,14 @@ def read_active_subscriptions( *, scsAsId: str = Path(..., title="The ID of the Netapp that read all the subscriptions", example="myNetapp"), - db_mongo: Database = Depends(deps.get_mongo_db), current_user: models.User = Depends(deps.get_current_active_user), http_request: Request ) -> Any: """ Read all active subscriptions """ + db_mongo = client.fastapi + retrieved_docs = crud_mongo.read_all(db_mongo, db_collection, current_user.id) temp_json_subs = retrieved_docs.copy() #Create copy of the list (json_subs) -> you cannot remove items from a list while you iterating the list. @@ -58,7 +60,6 @@ def create_subscription( *, scsAsId: str = Path(..., title="The ID of the Netapp that creates a subscription", example="myNetapp"), db: Session = Depends(deps.get_db), - db_mongo: Database = Depends(deps.get_mongo_db), item_in: schemas.MonitoringEventSubscriptionCreate, current_user: models.User = Depends(deps.get_current_active_user), http_request: Request @@ -66,6 +67,8 @@ def create_subscription( """ Create new subscription. """ + db_mongo = client.fastapi + UE = ue.get_externalId(db=db, externalId=str(item_in.externalId), owner_id=current_user.id) if not UE: raise HTTPException(status_code=409, detail="UE with this external identifier doesn't exist") @@ -122,7 +125,7 @@ def create_subscription( add_notifications(http_request, http_response, False) return http_response - elif item_in.monitoringType == "LOSS_OF_CONNECTIVITY" and item_in.maximumNumberOfReports == 1: + elif (item_in.monitoringType == "LOSS_OF_CONNECTIVITY" or item_in.monitoringType == "UE_REACHABILITY") and item_in.maximumNumberOfReports == 1: return JSONResponse(content=jsonable_encoder( { "title" : "The requested parameters are out of range", @@ -132,7 +135,7 @@ def create_subscription( } } ), status_code=403) - elif item_in.monitoringType == "LOSS_OF_CONNECTIVITY" and item_in.maximumNumberOfReports > 1: + elif (item_in.monitoringType == "LOSS_OF_CONNECTIVITY" or item_in.monitoringType == "UE_REACHABILITY") and item_in.maximumNumberOfReports > 1: #Check if subscription with externalid && monitoringType exists if crud_mongo.read_by_multiple_pairs(db_mongo, db_collection, externalId = item_in.externalId, monitoringType = item_in.monitoringType): raise HTTPException(status_code=409, detail=f"There is already an active subscription for UE with external id {item_in.externalId} - Monitoring Type = {item_in.monitoringType}") @@ -165,7 +168,6 @@ def update_subscription( *, scsAsId: str = Path(..., title="The ID of the Netapp that creates a subscription", example="myNetapp"), subscriptionId: str = Path(..., title="Identifier of the subscription resource"), - db_mongo: Database = Depends(deps.get_mongo_db), item_in: schemas.MonitoringEventSubscriptionCreate, current_user: models.User = Depends(deps.get_current_active_user), http_request: Request @@ -173,6 +175,8 @@ def update_subscription( """ Update/Replace an existing subscription resource """ + db_mongo = client.fastapi + try: retrieved_doc = crud_mongo.read_uuid(db_mongo, db_collection, subscriptionId) except Exception as ex: @@ -209,13 +213,14 @@ def read_subscription( *, scsAsId: str = Path(..., title="The ID of the Netapp that creates a subscription", example="myNetapp"), subscriptionId: str = Path(..., title="Identifier of the subscription resource"), - db_mongo: Database = Depends(deps.get_mongo_db), current_user: models.User = Depends(deps.get_current_active_user), http_request: Request ) -> Any: """ Get subscription by id """ + db_mongo = client.fastapi + try: retrieved_doc = crud_mongo.read_uuid(db_mongo, db_collection, subscriptionId) except Exception as ex: @@ -245,13 +250,14 @@ def delete_subscription( *, scsAsId: str = Path(..., title="The ID of the Netapp that creates a subscription", example="myNetapp"), subscriptionId: str = Path(..., title="Identifier of the subscription resource"), - db_mongo: Database = Depends(deps.get_mongo_db), current_user: models.User = Depends(deps.get_current_active_user), http_request: Request ) -> Any: """ Delete a subscription """ + db_mongo = client.fastapi + try: retrieved_doc = crud_mongo.read_uuid(db_mongo, db_collection, subscriptionId) except Exception as ex: diff --git a/backend/app/app/api/api_v1/endpoints/qosInformation.py b/backend/app/app/api/api_v1/endpoints/qosInformation.py index 3ddfc74c..517b8557 100644 --- a/backend/app/app/api/api_v1/endpoints/qosInformation.py +++ b/backend/app/app/api/api_v1/endpoints/qosInformation.py @@ -3,7 +3,7 @@ from fastapi.responses import JSONResponse from pymongo.database import Database from sqlalchemy.orm.session import Session - +from app.db.session import client from app import models from app.api import deps from app.core.config import qosSettings @@ -53,12 +53,13 @@ def read_qos_active_profiles( gNB_id: str = Path(..., title="The ID of the gNB", example="AAAAA1"), current_user: models.User = Depends(deps.get_current_active_user), http_request: Request, - db_mongo: Database = Depends(deps.get_mongo_db), db: Session = Depends(deps.get_db) ) -> Any: """ Get the available QoS Characteristics """ + db_mongo = client.fastapi + gNB = gnb.get_gNB_id(db=db, id=gNB_id) if not gNB: raise HTTPException(status_code=404, detail="gNB not found") diff --git a/backend/app/app/api/api_v1/endpoints/qosMonitoring.py b/backend/app/app/api/api_v1/endpoints/qosMonitoring.py index a8172320..fdeb3e33 100644 --- a/backend/app/app/api/api_v1/endpoints/qosMonitoring.py +++ b/backend/app/app/api/api_v1/endpoints/qosMonitoring.py @@ -8,6 +8,7 @@ from app import models, schemas from app.api import deps from app.crud import crud_mongo, user, ue +from app.db.session import client from .utils import add_notifications from .qosInformation import qos_reference_match @@ -18,13 +19,13 @@ def read_active_subscriptions( *, scsAsId: str = Path(..., title="The ID of the Netapp that creates a subscription", example="myNetapp"), - db_mongo: Database = Depends(deps.get_mongo_db), current_user: models.User = Depends(deps.get_current_active_user), http_request: Request ) -> Any: """ Get subscription by id """ + db_mongo = client.fastapi retrieved_docs = crud_mongo.read_all(db_mongo, db_collection, current_user.id) #Check if there are any active subscriptions @@ -47,22 +48,25 @@ def monitoring_notification(body: schemas.UserPlaneNotificationData): def create_subscription( *, scsAsId: str = Path(..., title="The ID of the Netapp that creates a subscription", example="myNetapp"), - db_mongo: Database = Depends(deps.get_mongo_db), db: Session = Depends(deps.get_db), item_in: schemas.AsSessionWithQoSSubscriptionCreate, current_user: models.User = Depends(deps.get_current_active_user), http_request: Request ) -> Any: + db_mongo = client.fastapi + json_request = jsonable_encoder(item_in) #Currently only EVENT_TRIGGERED is supported fiveG_qi = qos_reference_match(item_in.qosReference) if fiveG_qi.get('type') == 'GBR' or fiveG_qi.get('type') == 'DC-GBR': if (json_request['qosMonInfo'] == None) or (json_request['qosMonInfo']['repFreqs'] == None): raise HTTPException(status_code=400, detail="Please enter a value in repFreqs field") - else: - if 'EVENT_TRIGGERED' not in json_request['qosMonInfo']['repFreqs']: - raise HTTPException(status_code=400, detail="Only 'EVENT_TRIGGERED' reporting frequency is supported at the current version. Please enter 'EVENT_TRIGGERED' in repFreqs field") + + print(f'------------------------------------Curl from script {item_in.ipv4Addr}') + # else: + # if 'EVENT_TRIGGERED' not in json_request['qosMonInfo']['repFreqs']: + # raise HTTPException(status_code=400, detail="Only 'EVENT_TRIGGERED' reporting frequency is supported at the current version. Please enter 'EVENT_TRIGGERED' in repFreqs field") #Ensure that the user sends only one of the ipv4, ipv6, macAddr fields @@ -134,13 +138,13 @@ def read_subscription( *, scsAsId: str = Path(..., title="The ID of the Netapp that creates a subscription", example="myNetapp"), subscriptionId: str = Path(..., title="Identifier of the subscription resource"), - db_mongo: Database = Depends(deps.get_mongo_db), current_user: models.User = Depends(deps.get_current_active_user), http_request: Request ) -> Any: """ Get subscription by id """ + db_mongo = client.fastapi try: retrieved_doc = crud_mongo.read_uuid(db_mongo, db_collection, subscriptionId) @@ -165,13 +169,13 @@ def update_subscription( scsAsId: str = Path(..., title="The ID of the Netapp that creates a subscription", example="myNetapp"), subscriptionId: str = Path(..., title="Identifier of the subscription resource"), item_in: schemas.AsSessionWithQoSSubscriptionCreate, - db_mongo: Database = Depends(deps.get_mongo_db), current_user: models.User = Depends(deps.get_current_active_user), http_request: Request ) -> Any: """ Update subscription by id """ + db_mongo = client.fastapi try: retrieved_doc = crud_mongo.read_uuid(db_mongo, db_collection, subscriptionId) @@ -201,13 +205,14 @@ def delete_subscription( *, scsAsId: str = Path(..., title="The ID of the Netapp that creates a subscription", example="myNetapp"), subscriptionId: str = Path(..., title="Identifier of the subscription resource"), - db_mongo: Database = Depends(deps.get_mongo_db), current_user: models.User = Depends(deps.get_current_active_user), http_request: Request ) -> Any: """ Delete a subscription """ + db_mongo = client.fastapi + try: retrieved_doc = crud_mongo.read_uuid(db_mongo, db_collection, subscriptionId) except Exception as ex: diff --git a/backend/app/app/api/api_v1/endpoints/ue_movement.py b/backend/app/app/api/api_v1/endpoints/ue_movement.py index af639083..b6d6f525 100644 --- a/backend/app/app/api/api_v1/endpoints/ue_movement.py +++ b/backend/app/app/api/api_v1/endpoints/ue_movement.py @@ -7,10 +7,11 @@ from app.crud import crud_mongo from app.tools.distance import check_distance from app.tools import qos_callback -from app.db.session import SessionLocal +from app.db.session import SessionLocal, client from app.api import deps from app.schemas import Msg from app.tools import monitoring_callbacks, timer +from sqlalchemy.orm import Session #Dictionary holding threads that are running per user id. threads = {} @@ -25,20 +26,26 @@ def __init__(self, group=None, target=None, name=None, args=(), kwargs=None): self._args = args self._kwargs = kwargs self._stop_threads = False + self._db = SessionLocal() return def run(self): current_user = self._args[0] supi = self._args[1] + + active_subscriptions = { + "location_reporting" : False, + "ue_reachability" : False, + "loss_of_connectivity" : False, + "as_session_with_qos" : False + } try: - db = SessionLocal() - client = MongoClient("mongodb://mongo:27017", username='root', password='pass') db_mongo = client.fastapi #Initiate UE - if exists - UE = crud.ue.get_supi(db=db, supi=supi) + UE = crud.ue.get_supi(db=self._db, supi=supi) if not UE: logging.warning("UE not found") threads.pop(f"{supi}") @@ -63,7 +70,7 @@ def run(self): #Retrieve paths & points - path = crud.path.get(db=db, id=UE.path_id) + path = crud.path.get(db=self._db, id=UE.path_id) if not path: logging.warning("Path not found") threads.pop(f"{supi}") @@ -73,16 +80,19 @@ def run(self): threads.pop(f"{supi}") return - points = crud.points.get_points(db=db, path_id=UE.path_id) + points = crud.points.get_points(db=self._db, path_id=UE.path_id) points = jsonable_encoder(points) #Retrieve all the cells - Cells = crud.cell.get_multi_by_owner(db=db, owner_id=current_user.id, skip=0, limit=100) + Cells = crud.cell.get_multi_by_owner(db=self._db, owner_id=current_user.id, skip=0, limit=100) json_cells = jsonable_encoder(Cells) is_superuser = crud.user.is_superuser(current_user) - t = timer.Timer() + t = timer.SequencialTimer(logger=logging.critical) + + # global loss_of_connectivity_ack + loss_of_connectivity_ack = "FALSE" ''' =================================================================== 2nd Approach for updating UEs position @@ -134,79 +144,164 @@ def run(self): logging.warning("Failed to update coordinates") logging.warning(ex) + + #MonitoringEvent API - Loss of connectivity + if not active_subscriptions.get("loss_of_connectivity"): + loss_of_connectivity_sub = crud_mongo.read_by_multiple_pairs(db_mongo, "MonitoringEvent", externalId = UE.external_identifier, monitoringType = "LOSS_OF_CONNECTIVITY") + if loss_of_connectivity_sub: + active_subscriptions.update({"loss_of_connectivity" : True}) + + + #Validation of subscription + if active_subscriptions.get("loss_of_connectivity") and loss_of_connectivity_ack == "FALSE": + sub_is_valid = monitoring_event_sub_validation(loss_of_connectivity_sub, is_superuser, current_user.id, loss_of_connectivity_sub.get("owner_id")) + if sub_is_valid: + try: + try: + elapsed_time = t.status() + if elapsed_time > loss_of_connectivity_sub.get("maximumDetectionTime"): + response = monitoring_callbacks.loss_of_connectivity_callback(ues[f"{supi}"], loss_of_connectivity_sub.get("notificationDestination"), loss_of_connectivity_sub.get("link")) + + logging.critical(response.json()) + #This ack is used to send one time the loss of connectivity callback + loss_of_connectivity_ack = response.json().get("ack") + + loss_of_connectivity_sub.update({"maximumNumberOfReports" : loss_of_connectivity_sub.get("maximumNumberOfReports") - 1}) + crud_mongo.update(db_mongo, "MonitoringEvent", loss_of_connectivity_sub.get("_id"), loss_of_connectivity_sub) + except timer.TimerError as ex: + # logging.critical(ex) + pass + except requests.exceptions.ConnectionError as ex: + logging.warning("Failed to send the callback request") + logging.warning(ex) + crud_mongo.delete_by_uuid(db_mongo, "MonitoringEvent", loss_of_connectivity_sub.get("_id")) + active_subscriptions.update({"loss_of_connectivity" : False}) + continue + else: + crud_mongo.delete_by_uuid(db_mongo, "MonitoringEvent", loss_of_connectivity_sub.get("_id")) + active_subscriptions.update({"loss_of_connectivity" : False}) + logging.warning("Subscription has expired") + #MonitoringEvent API - Loss of connectivity + + #As Session With QoS API - search for active subscription in db + if not active_subscriptions.get("as_session_with_qos"): + qos_sub = crud_mongo.read(db_mongo, 'QoSMonitoring', 'ipv4Addr', UE.ip_address_v4) + if qos_sub: + active_subscriptions.update({"as_session_with_qos" : True}) + reporting_freq = qos_sub["qosMonInfo"]["repFreqs"] + reporting_period = qos_sub["qosMonInfo"]["repPeriod"] + if "PERIODIC" in reporting_freq: + rt = timer.RepeatedTimer(reporting_period, qos_callback.qos_notification_control, qos_sub, ues[f"{supi}"]["ip_address_v4"], ues.copy(), ues[f"{supi}"]) + # qos_callback.qos_notification_control(qos_sub, ues[f"{supi}"]["ip_address_v4"], ues.copy(), ues[f"{supi}"]) + + + #If the document exists then validate the owner + if not is_superuser and (qos_sub['owner_id'] != current_user.id): + logging.warning("Not enough permissions") + active_subscriptions.update({"as_session_with_qos" : False}) + #As Session With QoS API - search for active subscription in db + if cell_now != None: try: t.stop() + loss_of_connectivity_ack = "FALSE" + rt.start() except timer.TimerError as ex: # logging.critical(ex) pass # if UE.Cell_id != cell_now.get('id'): #Cell has changed in the db "handover" if ues[f"{supi}"]["Cell_id"] != cell_now.get('id'): #Cell has changed in the db "handover" + + #Monitoring Event API - UE reachability + #check if the ue was disconnected before + if ues[f"{supi}"]["Cell_id"] == None: + + if not active_subscriptions.get("ue_reachability"): + ue_reachability_sub = crud_mongo.read_by_multiple_pairs(db_mongo, "MonitoringEvent", externalId = UE.external_identifier, monitoringType = "UE_REACHABILITY") + if ue_reachability_sub: + active_subscriptions.update({"ue_reachability" : True}) + + #Validation of subscription + + if active_subscriptions.get("ue_reachability"): + sub_is_valid = monitoring_event_sub_validation(ue_reachability_sub, is_superuser, current_user.id, ue_reachability_sub.get("owner_id")) + if sub_is_valid: + try: + try: + monitoring_callbacks.ue_reachability_callback(ues[f"{supi}"], ue_reachability_sub.get("notificationDestination"), ue_reachability_sub.get("link"), ue_reachability_sub.get("reachabilityType")) + ue_reachability_sub.update({"maximumNumberOfReports" : ue_reachability_sub.get("maximumNumberOfReports") - 1}) + crud_mongo.update(db_mongo, "MonitoringEvent", ue_reachability_sub.get("_id"), ue_reachability_sub) + except timer.TimerError as ex: + # logging.critical(ex) + pass + except requests.exceptions.ConnectionError as ex: + logging.warning("Failed to send the callback request") + logging.warning(ex) + crud_mongo.delete_by_uuid(db_mongo, "MonitoringEvent", ue_reachability_sub.get("_id")) + active_subscriptions.update({"ue_reachability" : False}) + continue + else: + crud_mongo.delete_by_uuid(db_mongo, "MonitoringEvent", ue_reachability_sub.get("_id")) + active_subscriptions.update({"ue_reachability" : False}) + logging.warning("Subscription has expired") + #Monitoring Event API - UE reachability + + # logging.warning(f"UE({UE.supi}) with ipv4 {UE.ip_address_v4} handovers to Cell {cell_now.get('id')}, {cell_now.get('description')}") - # crud.ue.update(db=db, db_obj=UE, obj_in={"Cell_id" : cell_now.get('id')}) ues[f"{supi}"]["Cell_id"] = cell_now.get('id') ues[f"{supi}"]["cell_id_hex"] = cell_now.get('cell_id') - gnb = crud.gnb.get(db=db, id=cell_now.get("gNB_id")) + gnb = crud.gnb.get(db=self._db, id=cell_now.get("gNB_id")) ues[f"{supi}"]["gnb_id_hex"] = gnb.gNB_id - #Retrieve the subscription of the UE by external Id | This could be outside while true but then the user cannot subscribe when the loop runs - # sub = crud.monitoring.get_sub_externalId(db=db, externalId=UE.external_identifier, owner_id=current_user.id) - sub = crud_mongo.read(db_mongo, "MonitoringEvent", "externalId", UE.external_identifier) - #Validation of subscription - if not sub: - # logging.warning("Monitoring Event subscription not found") - pass - elif not is_superuser and (sub.get("owner_id") != current_user.id): - # logging.warning("Not enough permissions") - pass - else: - sub_validate_time = tools.check_expiration_time(expire_time=sub.get("monitorExpireTime")) - if sub_validate_time: - sub = tools.check_numberOfReports(db_mongo, sub) - if sub: #return the callback request only if subscription is valid + #Monitoring Event API - Location Reporting + #Retrieve the subscription of the UE by external Id | This could be outside while true but then the user cannot subscribe when the loop runs + if not active_subscriptions.get("location_reporting"): + location_reporting_sub = crud_mongo.read_by_multiple_pairs(db_mongo, "MonitoringEvent", externalId = UE.external_identifier, monitoringType = "LOCATION_REPORTING") + if location_reporting_sub: + active_subscriptions.update({"location_reporting" : True}) + + #Validation of subscription + if active_subscriptions.get("location_reporting"): + sub_is_valid = monitoring_event_sub_validation(location_reporting_sub, is_superuser, current_user.id, location_reporting_sub.get("owner_id")) + if sub_is_valid: + try: try: - response = monitoring_callbacks.location_callback(ues[f"{supi}"], sub.get("notificationDestination"), sub.get("link")) - # logging.info(response.json()) - except requests.exceptions.ConnectionError as ex: - logging.warning("Failed to send the callback request") - logging.warning(ex) - crud_mongo.delete_by_uuid(db_mongo, "MonitoringEvent", sub.get("_id")) - continue + monitoring_callbacks.location_callback(ues[f"{supi}"], location_reporting_sub.get("notificationDestination"), location_reporting_sub.get("link")) + location_reporting_sub.update({"maximumNumberOfReports" : location_reporting_sub.get("maximumNumberOfReports") - 1}) + crud_mongo.update(db_mongo, "MonitoringEvent", location_reporting_sub.get("_id"), location_reporting_sub) + except timer.TimerError as ex: + # logging.critical(ex) + pass + except requests.exceptions.ConnectionError as ex: + logging.warning("Failed to send the callback request") + logging.warning(ex) + crud_mongo.delete_by_uuid(db_mongo, "MonitoringEvent", location_reporting_sub.get("_id")) + active_subscriptions.update({"location_reporting" : False}) + continue else: - crud_mongo.delete_by_uuid(db_mongo, "MonitoringEvent", sub.get("_id")) - logging.warning("Subscription has expired (expiration date)") - - #QoS Monitoring Event (handover) - # ues_connected = crud.ue.get_by_Cell(db=db, cell_id=UE.Cell_id) - ues_connected = 0 - # temp_ues = ues.copy() - # for ue in temp_ues: - # # print(ue) - # if ues[ue]["Cell_id"] == ues[f"{supi}"]["Cell_id"]: - # ues_connected += 1 - - #subtract 1 for the UE that is currently running. We are looking for other ues that are currently connected in the same cell - ues_connected -= 1 - - if ues_connected > 1: - gbr = 'QOS_NOT_GUARANTEED' - else: - gbr = 'QOS_GUARANTEED' - - # logging.warning(gbr) - # qos_notification_control(gbr ,current_user, UE.ip_address_v4) - qos_callback.qos_notification_control(current_user, ues[f"{supi}"]["ip_address_v4"], ues.copy(), ues[f"{supi}"]) - + crud_mongo.delete_by_uuid(db_mongo, "MonitoringEvent", location_reporting_sub.get("_id")) + active_subscriptions.update({"location_reporting" : False}) + logging.warning("Subscription has expired") + #Monitoring Event API - Location Reporting + + #As Session With QoS API - if EVENT_TRIGGER then send callback on handover + if active_subscriptions.get("as_session_with_qos"): + reporting_freq = qos_sub["qosMonInfo"]["repFreqs"] + if "EVENT_TRIGGERED" in reporting_freq: + qos_callback.qos_notification_control(qos_sub, ues[f"{supi}"]["ip_address_v4"], ues.copy(), ues[f"{supi}"]) + #As Session With QoS API - if EVENT_TRIGGER then send callback on handover + else: # crud.ue.update(db=db, db_obj=UE, obj_in={"Cell_id" : None}) try: t.start() + rt.stop() except timer.TimerError as ex: # logging.critical(ex) pass - + ues[f"{supi}"]["Cell_id"] = None ues[f"{supi}"]["cell_id_hex"] = None ues[f"{supi}"]["gnb_id_hex"] = None @@ -227,9 +322,11 @@ def run(self): if self._stop_threads: logging.critical("Terminating thread...") - crud.ue.update_coordinates(db=db, lat=ues[f"{supi}"]["latitude"], long=ues[f"{supi}"]["longitude"], db_obj=UE) - crud.ue.update(db=db, db_obj=UE, obj_in={"Cell_id" : ues[f"{supi}"]["Cell_id"]}) + crud.ue.update_coordinates(db=self._db, lat=ues[f"{supi}"]["latitude"], long=ues[f"{supi}"]["longitude"], db_obj=UE) + crud.ue.update(db=self._db, db_obj=UE, obj_in={"Cell_id" : ues[f"{supi}"]["Cell_id"]}) ues.pop(f"{supi}") + self._db.close() + rt.stop() break # End of 2nd Approach for updating UEs position @@ -275,58 +372,7 @@ def run(self): # continue - # try: - # UE = crud.ue.update_coordinates(db=db, lat=point["latitude"], long=point["longitude"], db_obj=UE) - # cell_now = check_distance(UE.latitude, UE.longitude, json_cells) #calculate the distance from all the cells - # except Exception as ex: - # logging.warning("Failed to update coordinates") - # logging.warning(ex) - - # if cell_now != None: - # if UE.Cell_id != cell_now.get('id'): #Cell has changed in the db "handover" - # logging.warning(f"UE({UE.supi}) with ipv4 {UE.ip_address_v4} handovers to Cell {cell_now.get('id')}, {cell_now.get('description')}") - # crud.ue.update(db=db, db_obj=UE, obj_in={"Cell_id" : cell_now.get('id')}) - - # #Retrieve the subscription of the UE by external Id | This could be outside while true but then the user cannot subscribe when the loop runs - # # sub = crud.monitoring.get_sub_externalId(db=db, externalId=UE.external_identifier, owner_id=current_user.id) - # sub = crud_mongo.read(db_mongo, "MonitoringEvent", "externalId", UE.external_identifier) - - # #Validation of subscription - # if not sub: - # logging.warning("Monitoring Event subscription not found") - # elif not crud.user.is_superuser(current_user) and (sub.get("owner_id") != current_user.id): - # logging.warning("Not enough permissions") - # else: - # sub_validate_time = tools.check_expiration_time(expire_time=sub.get("monitorExpireTime")) - # if sub_validate_time: - # sub = tools.check_numberOfReports(db_mongo, sub) - # if sub: #return the callback request only if subscription is valid - # try: - # response = location_callback(UE, sub.get("notificationDestination"), sub.get("link")) - # logging.info(response.json()) - # except requests.exceptions.ConnectionError as ex: - # logging.warning("Failed to send the callback request") - # logging.warning(ex) - # crud_mongo.delete_by_uuid(db_mongo, "MonitoringEvent", sub.get("_id")) - # continue - # else: - # crud_mongo.delete_by_uuid(db_mongo, "MonitoringEvent", sub.get("_id")) - # logging.warning("Subscription has expired (expiration date)") - - # #QoS Monitoring Event (handover) - # ues_connected = crud.ue.get_by_Cell(db=db, cell_id=UE.Cell_id) - # if len(ues_connected) > 1: - # gbr = 'QOS_NOT_GUARANTEED' - # else: - # gbr = 'QOS_GUARANTEED' - - # logging.warning(gbr) - # qos_notification_control(gbr ,current_user, UE.ip_address_v4) - # logging.critical("Bypassed qos notification control") - # else: - # crud.ue.update(db=db, db_obj=UE, obj_in={"Cell_id" : None}) - - # # logging.info(f'User: {current_user.id} | UE: {supi} | Current location: latitude ={UE.latitude} | longitude = {UE.longitude} | Speed: {UE.speed}' ) + # #-----------------------Code goes here-------------------------# # if UE.speed == 'LOW': # time.sleep(1) @@ -340,9 +386,9 @@ def run(self): # if self._stop_threads: # print("Terminating thread...") # break - finally: - db.close() - return + + except Exception as ex: + logging.critical(ex) def stop(self): self._stop_threads = True @@ -421,3 +467,15 @@ def retrieve_ue(supi: str) -> dict: return ues.get(supi) +def monitoring_event_sub_validation(sub: dict, is_superuser: bool, current_user_id: int, owner_id) -> bool: + + if not is_superuser and (owner_id != current_user_id): + # logging.warning("Not enough permissions") + return False + else: + sub_validate_time = tools.check_expiration_time(expire_time=sub.get("monitorExpireTime")) + sub_validate_number_of_reports = tools.check_numberOfReports(sub.get("maximumNumberOfReports")) + if sub_validate_time and sub_validate_number_of_reports: + return True + else: + return False \ No newline at end of file diff --git a/backend/app/app/api/deps.py b/backend/app/app/api/deps.py index 75fd4933..6247ae0f 100644 --- a/backend/app/app/api/deps.py +++ b/backend/app/app/api/deps.py @@ -1,5 +1,4 @@ from typing import Generator -from pymongo import MongoClient from fastapi import Depends, HTTPException, status from fastapi.security import OAuth2PasswordBearer from jose import jwt @@ -23,14 +22,6 @@ def get_db() -> Generator: finally: db.close() #The code following the yield statement is executed after the response has been delivered -#Dependency for mongoDB - -def get_mongo_db(): - client = MongoClient("mongodb://mongo:27017", username='root', password='pass') - db = client.fastapi - return db - - def get_current_user( db: Session = Depends(get_db), token: str = Depends(reusable_oauth2) ) -> models.User: diff --git a/backend/app/app/db/session.py b/backend/app/app/db/session.py index 1e6b7f18..7b5b00be 100644 --- a/backend/app/app/db/session.py +++ b/backend/app/app/db/session.py @@ -1,7 +1,10 @@ from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker - +from pymongo import MongoClient from app.core.config import settings engine = create_engine(settings.SQLALCHEMY_DATABASE_URI, pool_pre_ping=True, pool_size=150, max_overflow=20) #Create a db URL for SQLAlchemy in core/config.py/ Settings class SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) #Each instance is a db session + + +client = MongoClient("mongodb://mongo:27017", username='root', password='pass') diff --git a/backend/app/app/schemas/monitoringevent.py b/backend/app/app/schemas/monitoringevent.py index 1f844862..272ad84a 100644 --- a/backend/app/app/schemas/monitoringevent.py +++ b/backend/app/app/schemas/monitoringevent.py @@ -21,7 +21,11 @@ class LocationInfo(BaseModel): class MonitoringType(str, Enum): locationReporting = "LOCATION_REPORTING" lossOfConnectivity = "LOSS_OF_CONNECTIVITY" + ueReachability = "UE_REACHABILITY" +class ReachabilityType(str, Enum): + sms = "SMS" + data = "DATA" class MonitoringEventReport(BaseModel): # msisdn: Optional[str] = None @@ -44,8 +48,8 @@ class MonitoringEventSubscriptionCreate(BaseModel): monitoringType: MonitoringType maximumNumberOfReports: Optional[int] = Field(None, description="Identifies the maximum number of event reports to be generated. Value 1 makes the Monitoring Request a One-time Request", ge=1) monitorExpireTime: Optional[datetime] = Field(None, description="Identifies the absolute time at which the related monitoring event request is considered to expire") - maximumDetectionTime: Optional[int] = Field(None, description="If monitoringType is \"LOSS_OF_CONNECTIVITY\", this parameter may be included to identify the maximum period of time after which the UE is considered to be unreachable.", gt=0) - # monitoringEventReport: Optional[MonitoringEventReport] = None + maximumDetectionTime: Optional[int] = Field(1, description="If monitoringType is \"LOSS_OF_CONNECTIVITY\", this parameter may be included to identify the maximum period of time after which the UE is considered to be unreachable.", gt=0) + reachabilityType: Optional[ReachabilityType] = Field("DATA", description="If monitoringType is \"UE_REACHABILITY\", this parameter shall be included to identify whether the request is for \"Reachability for SMS\" or \"Reachability for Data\"") class MonitoringEventSubscription(MonitoringEventSubscriptionCreate): link: Optional[AnyHttpUrl] = Field("https://myresource.com", description="String identifying a referenced resource. This is also returned as a location header in 201 Created Response") @@ -56,6 +60,7 @@ class Config: class MonitoringNotification(MonitoringEventReport): subscription: AnyHttpUrl lossOfConnectReason: Optional[int] = Field(None, description= "According to 3GPP TS 29.522 the lossOfConnectReason attribute shall be set to 6 if the UE is deregistered, 7 if the maximum detection timer expires or 8 if the UE is purged") + reachabilityType: Optional[ReachabilityType] = Field("DATA", description="If monitoringType is \"UE_REACHABILITY\", this parameter shall be included to identify whether the request is for \"Reachability for SMS\" or \"Reachability for Data\"") class MonitoringEventReportReceived(BaseModel): ok: bool \ No newline at end of file diff --git a/backend/app/app/schemas/qosMonitoring.py b/backend/app/app/schemas/qosMonitoring.py index cb991896..edfc7abe 100644 --- a/backend/app/app/schemas/qosMonitoring.py +++ b/backend/app/app/schemas/qosMonitoring.py @@ -40,7 +40,7 @@ class AsSessionWithQoSSubscriptionCreate(BaseModel): ipv4Addr: Optional[IPvAnyAddress] = Field(default='10.0.0.0', description="String identifying an Ipv4 address") ipv6Addr: Optional[IPvAnyAddress] = Field(default="0:0:0:0:0:0:0:0", description="String identifying an Ipv6 address. Default value ::1/128 (loopback)") macAddr: Optional[constr(regex=r'^([0-9a-fA-F]{2})((-[0-9a-fA-F]{2}){5})$')] = '22-00-00-00-00-00' - notificationDestination: AnyHttpUrl = Field(..., description="Reference resource (URL) identifying service consumer's endpoint, in order to receive the asynchronous notification. For testing use 'http://localhost:80/api/v1/utils/session-with-qos/callback'") #Default value for development testing + notificationDestination: AnyHttpUrl = Field("http://localhost:80/api/v1/utils/session-with-qos/callback", description="Reference resource (URL) identifying service consumer's endpoint, in order to receive the asynchronous notification. For testing use 'http://localhost:80/api/v1/utils/session-with-qos/callback'") #Default value for development testing snssai: Optional[Snssai] = None dnn: Optional[str] = Field("province1.mnc01.mcc202.gprs", description="String identifying the Data Network Name (i.e., Access Point Name in 4G). For more information check clause 9A of 3GPP TS 23.003") qosReference: int = Field(default=9, description="Identifies a pre-defined QoS Information", ge=1, le=90) diff --git a/backend/app/app/static/js/map.js b/backend/app/app/static/js/map.js index dabc71a4..04547180 100644 --- a/backend/app/app/static/js/map.js +++ b/backend/app/app/static/js/map.js @@ -474,7 +474,7 @@ function ui_map_paint_moving_UEs() { function api_get_Cells() { - var url = app.api_url + '/Cells/?skip=0&limit=100'; + var url = app.api_url + '/Cells?skip=0&limit=1000'; $.ajax({ type: 'GET', diff --git a/backend/app/app/tools/check_subscription.py b/backend/app/app/tools/check_subscription.py index 99e74343..974f45cf 100644 --- a/backend/app/app/tools/check_subscription.py +++ b/backend/app/app/tools/check_subscription.py @@ -52,17 +52,9 @@ def check_expiration_time(expire_time): else: return False -def check_numberOfReports(db, sub): - if sub.get("maximumNumberOfReports")>1: - newNumberOfReports = sub.get("maximumNumberOfReports") - 1 - sub.update({"maximumNumberOfReports" : newNumberOfReports}) - crud_mongo.update(db, "MonitoringEvent", sub.get("_id"), sub) - return sub - elif sub.get("maximumNumberOfReports") == 1: - newNumberOfReports = sub.get("maximumNumberOfReports") - 1 - sub.update({"maximumNumberOfReports" : newNumberOfReports}) - # monitoring.remove(db=db, id=item_in.id) - crud_mongo.delete_by_uuid(db, "MonitoringEvent", sub.get("_id")) - return sub +def check_numberOfReports(maximum_number_of_reports: int) -> bool: + if maximum_number_of_reports >= 1: + return True else: - logging.warning("Subscription has expired (maximum number of reports") \ No newline at end of file + logging.warning("Subscription has expired (maximum number of reports") + return False \ No newline at end of file diff --git a/backend/app/app/tools/monitoring_callbacks.py b/backend/app/app/tools/monitoring_callbacks.py index 57991a42..f129cf33 100644 --- a/backend/app/app/tools/monitoring_callbacks.py +++ b/backend/app/app/tools/monitoring_callbacks.py @@ -26,7 +26,7 @@ def location_callback(ue, callbackurl, subscription): return response -def loss_of_connectivity_callaback(ue, callbackurl, subscription): +def loss_of_connectivity_callback(ue, callbackurl, subscription): url = callbackurl payload = json.dumps({ @@ -34,7 +34,30 @@ def loss_of_connectivity_callaback(ue, callbackurl, subscription): "ipv4Addr" : ue.get("ip_address_v4"), "subscription" : subscription, "monitoringType": "LOSS_OF_CONNECTIVITY", - "lossOfConnectReason": 8 + "lossOfConnectReason": 7 + }) + headers = { + 'accept': 'application/json', + 'Content-Type': 'application/json' + } + + #Timeout values according to https://docs.python-requests.org/en/master/user/advanced/#timeouts + #First value of the tuple "3.05" corresponds to connect and second "27" to read timeouts + #(i.e., connect timeout means that the server is unreachable and read that the server is reachable but the client does not receive a response within 27 seconds) + + response = requests.request("POST", url, headers=headers, data=payload, timeout=(3.05, 27)) + + return response + +def ue_reachability_callback(ue, callbackurl, subscription, reachabilityType): + url = callbackurl + + payload = json.dumps({ + "externalId" : ue.get("external_identifier"), + "ipv4Addr" : ue.get("ip_address_v4"), + "subscription" : subscription, + "monitoringType": "UE_REACHABILITY", + "reachabilityType": reachabilityType }) headers = { 'accept': 'application/json', diff --git a/backend/app/app/tools/qos_callback.py b/backend/app/app/tools/qos_callback.py index 564ba18d..2e2c1a92 100644 --- a/backend/app/app/tools/qos_callback.py +++ b/backend/app/app/tools/qos_callback.py @@ -1,6 +1,5 @@ import requests, json, logging -from pymongo import MongoClient -from app.crud import crud_mongo, user, ue +from app.crud import ue from app.api.api_v1.endpoints.qosInformation import qos_reference_match from app.db.session import SessionLocal from fastapi.encoders import jsonable_encoder @@ -52,20 +51,7 @@ def qos_callback(callbackurl, resource, qos_status, ipv4): return response -def qos_notification_control(current_user, ipv4, ues: dict, current_ue: dict): - client = MongoClient("mongodb://mongo:27017", username='root', password='pass') - db = client.fastapi - - doc = crud_mongo.read(db, 'QoSMonitoring', 'ipv4Addr', ipv4) - - #Check if the document exists - if not doc: - # logging.warning("AsSessionWithQoS subscription not found") - return - #If the document exists then validate the owner - if not user.is_superuser(current_user) and (doc['owner_id'] != current_user.id): - logging.info("Not enough permissions") - return +def qos_notification_control(doc, ipv4, ues: dict, current_ue: dict): number_of_ues_in_cell = ues_in_cell(ues, current_ue) diff --git a/backend/app/app/tools/timer.py b/backend/app/app/tools/timer.py index 58705649..b1bdf0e3 100644 --- a/backend/app/app/tools/timer.py +++ b/backend/app/app/tools/timer.py @@ -1,11 +1,17 @@ -import time, logging +import time +from threading import Timer class TimerError(Exception): """A custom exception used to report errors in use of Timer class""" -class Timer: - def __init__(self): +class SequencialTimer: + def __init__(self, + text="Elapsed time: {:0.4f} seconds", + logger=print + ): self._start_time = None + self.text = text + self.logger = logger def start(self): """Start a new timer""" @@ -20,5 +26,48 @@ def stop(self): raise TimerError(f"Timer is not running. Use .start() to start it") elapsed_time = time.perf_counter() - self._start_time - self._start_time = None - logging.critical(f"Elapsed time: {elapsed_time:0.4f} seconds") \ No newline at end of file + # Reset the timer + self._start_time = None + + if self.logger: + self.logger(self.text.format(elapsed_time)) + + return elapsed_time + + def status(self): + """Report the elapsed time by the time timer has started""" + if self._start_time is None: + raise TimerError(f"Timer is not running. Use .start() to start it") + + elapsed_time = time.perf_counter() - self._start_time + + if self.logger: + self.logger(self.text.format(elapsed_time)) + + return elapsed_time + + +class RepeatedTimer(object): + def __init__(self, interval, function, *args, **kwargs): + self._timer = None + self.interval = interval + self.function = function + self.args = args + self.kwargs = kwargs + self.is_running = False + self.start() + + def _run(self): + self.is_running = False + self.start() + self.function(*self.args, **self.kwargs) + + def start(self): + if not self.is_running: + self._timer = Timer(self.interval, self._run) + self._timer.start() + self.is_running = True + + def stop(self): + self._timer.cancel() + self.is_running = False \ No newline at end of file diff --git a/backend/app/app/ui/dashboard.html b/backend/app/app/ui/dashboard.html index b106cb12..d4dc0507 100644 --- a/backend/app/app/ui/dashboard.html +++ b/backend/app/app/ui/dashboard.html @@ -41,111 +41,13 @@ - + +{% include "sidebar.html" ignore missing %} +
-
-
- - - - - - -
-
+ +{% include "header.html" ignore missing %} +
@@ -1077,10 +979,9 @@
Start / End points
-
-
© Evolved-5G
-
NCSR "Demokritos"
-
+ +{% include "footer.html" ignore missing %} +
diff --git a/backend/app/app/ui/export.html b/backend/app/app/ui/export.html index a840acd6..6e2a0c82 100644 --- a/backend/app/app/ui/export.html +++ b/backend/app/app/ui/export.html @@ -45,111 +45,13 @@ - + +{% include "sidebar.html" ignore missing %} +
-
-
- - - - - - -
-
+ +{% include "header.html" ignore missing %} +
@@ -188,10 +90,9 @@
-
-
© Evolved-5G
-
NCSR "Demokritos"
-
+ +{% include "footer.html" ignore missing %} +
diff --git a/backend/app/app/ui/footer.html b/backend/app/app/ui/footer.html new file mode 100644 index 00000000..700d95ba --- /dev/null +++ b/backend/app/app/ui/footer.html @@ -0,0 +1,9 @@ + \ No newline at end of file diff --git a/backend/app/app/ui/header.html b/backend/app/app/ui/header.html new file mode 100644 index 00000000..08de2f6d --- /dev/null +++ b/backend/app/app/ui/header.html @@ -0,0 +1,35 @@ +
+
+ + + + + + +
+
\ No newline at end of file diff --git a/backend/app/app/ui/import.html b/backend/app/app/ui/import.html index 04a29a29..9dba720d 100644 --- a/backend/app/app/ui/import.html +++ b/backend/app/app/ui/import.html @@ -45,111 +45,13 @@ - + +{% include "sidebar.html" ignore missing %} +
-
-
- - - - - - -
-
+ +{% include "header.html" ignore missing %} +
@@ -206,10 +108,9 @@
-
-
© Evolved-5G
-
NCSR "Demokritos"
-
+ +{% include "footer.html" ignore missing %} +
diff --git a/backend/app/app/ui/map.html b/backend/app/app/ui/map.html index 55aff8d4..8c1639f6 100644 --- a/backend/app/app/ui/map.html +++ b/backend/app/app/ui/map.html @@ -39,113 +39,13 @@ -
-
-
- - - - - - -
-
+ +{% include "header.html" ignore missing %} +
@@ -310,10 +210,9 @@

Response Body

-
-
© Evolved-5G
-
NCSR "Demokritos"
-
+ +{% include "footer.html" ignore missing %} +
diff --git a/backend/app/app/ui/sidebar.html b/backend/app/app/ui/sidebar.html new file mode 100644 index 00000000..343c0501 --- /dev/null +++ b/backend/app/app/ui/sidebar.html @@ -0,0 +1,69 @@ + \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index c588e799..5596dd25 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -45,10 +45,13 @@ services: restart: always ports: - 8081:8081 + env_file: + - .env environment: ME_CONFIG_MONGODB_ADMINUSERNAME: "${MONGO_USER}" ME_CONFIG_MONGODB_ADMINPASSWORD: "${MONGO_PASSWORD}" ME_CONFIG_MONGODB_URL: mongodb://${MONGO_USER}:${MONGO_PASSWORD}@mongo:27017/ + ME_CONFIG_MONGODB_ENABLE_ADMIN: "${MONGO_EXPRESS_ENABLE_ADMIN}" backend: diff --git a/env-file-for-local.dev b/env-file-for-local.dev index aded15ae..68b02d96 100644 --- a/env-file-for-local.dev +++ b/env-file-for-local.dev @@ -36,4 +36,7 @@ PGADMIN_DEFAULT_PASSWORD=pass # Mongo MONGO_USER=root -MONGO_PASSWORD=pass \ No newline at end of file +MONGO_PASSWORD=pass + +# MongoExpress +MONGO_EXPRESS_ENABLE_ADMIN=true \ No newline at end of file