diff --git a/manifests/nbiservice.yaml b/manifests/nbiservice.yaml index 5c954d1bb4d5f0fb53097fe3a8db51fb75bcccfe..108195d377d9958203ee36f1fe6e949eda34a197 100644 --- a/manifests/nbiservice.yaml +++ b/manifests/nbiservice.yaml @@ -23,9 +23,6 @@ spec: replicas: 1 template: metadata: - annotations: - config.linkerd.io/skip-inbound-ports: "8762" - config.linkerd.io/skip-outbound-ports: "8762" labels: app: nbiservice spec: @@ -36,22 +33,28 @@ spec: imagePullPolicy: Always ports: - containerPort: 8080 - - containerPort: 9090 - containerPort: 9192 - - containerPort: 8761 env: - name: LOG_LEVEL value: "INFO" + - name: FLASK_ENV + value: "production" # change to "development" if developing - name: IETF_NETWORK_RENDERER value: "LIBYANG" - - name: WS_IP_PORT - value: "8761" readinessProbe: - exec: - command: ["/bin/grpc_health_probe", "-addr=:9090"] + httpGet: + path: /healthz + port: 8080 + initialDelaySeconds: 5 + periodSeconds: 10 + failureThreshold: 3 livenessProbe: - exec: - command: ["/bin/grpc_health_probe", "-addr=:9090"] + httpGet: + path: /healthz + port: 8080 + initialDelaySeconds: 5 + periodSeconds: 10 + failureThreshold: 3 resources: requests: cpu: 50m @@ -75,15 +78,7 @@ spec: protocol: TCP port: 8080 targetPort: 8080 - - name: grpc - protocol: TCP - port: 9090 - targetPort: 9090 - name: metrics protocol: TCP port: 9192 targetPort: 9192 - - name: websocket - protocol: TCP - port: 8761 - targetPort: 8761 diff --git a/proto/nbi.proto b/proto/nbi.proto deleted file mode 100644 index a81df70ef20e0e3389047a9fd8a6bab927b74114..0000000000000000000000000000000000000000 --- a/proto/nbi.proto +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright 2022-2024 ETSI SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/) -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -syntax = "proto3"; -package nbi; - -import "context.proto"; - -service NbiService { - rpc CheckCredentials (context.TeraFlowController) returns (context.AuthenticationResult) {} - rpc GetConnectivityServiceStatus (context.ServiceId ) returns (context.ServiceStatus ) {} - rpc CreateConnectivityService (context.Service ) returns (context.ServiceId ) {} - rpc EditConnectivityService (context.Service ) returns (context.ServiceId ) {} - rpc DeleteConnectivityService (context.Service ) returns (context.Empty ) {} - rpc GetAllActiveConnectivityServices (context.Empty ) returns (context.ServiceIdList ) {} - rpc ClearAllConnectivityServices (context.Empty ) returns (context.Empty ) {} -} diff --git a/src/common/Constants.py b/src/common/Constants.py index 68200764610b5fded328dbec741fdbbf70bc0930..6508ac151e489db71d3f2ef176b220b23a420186 100644 --- a/src/common/Constants.py +++ b/src/common/Constants.py @@ -87,7 +87,6 @@ DEFAULT_SERVICE_GRPC_PORTS = { ServiceNameEnum.POLICY .value : 6060, ServiceNameEnum.MONITORING .value : 7070, ServiceNameEnum.DLT .value : 8080, - ServiceNameEnum.NBI .value : 9090, ServiceNameEnum.L3_CAD .value : 10001, ServiceNameEnum.L3_AM .value : 10002, ServiceNameEnum.DBSCANSERVING .value : 10008, diff --git a/src/nbi/.gitlab-ci.yml b/src/nbi/.gitlab-ci.yml index 71bf223ba9408e178e252d600c625dc2256dbe92..d97fab701c9560ea3d7e5c04b60bbf35292a9d9b 100644 --- a/src/nbi/.gitlab-ci.yml +++ b/src/nbi/.gitlab-ci.yml @@ -62,7 +62,7 @@ unit_test nbi: fi script: - docker pull "$CI_REGISTRY_IMAGE/$IMAGE_NAME:$IMAGE_TAG" - - docker run --name $IMAGE_NAME -d -p 9090:9090 -v "$PWD/src/$IMAGE_NAME/tests:/opt/results" --network=teraflowbridge $CI_REGISTRY_IMAGE/$IMAGE_NAME:$IMAGE_TAG + - docker run --name $IMAGE_NAME -d -v "$PWD/src/$IMAGE_NAME/tests:/opt/results" --network=teraflowbridge $CI_REGISTRY_IMAGE/$IMAGE_NAME:$IMAGE_TAG - sleep 5 - docker ps -a - docker logs $IMAGE_NAME diff --git a/src/nbi/Dockerfile b/src/nbi/Dockerfile index a9be06d37c15757f51d6d849f395d683885e9508..5f5aef914c7f20df4bb2022b424cafad0a417bc7 100644 --- a/src/nbi/Dockerfile +++ b/src/nbi/Dockerfile @@ -16,7 +16,7 @@ FROM python:3.9-slim # Install dependencies RUN apt-get --yes --quiet --quiet update && \ - apt-get --yes --quiet --quiet install wget g++ git build-essential cmake libpcre2-dev python3-dev python3-cffi && \ + apt-get --yes --quiet --quiet install g++ git build-essential cmake libpcre2-dev python3-dev python3-cffi && \ rm -rf /var/lib/apt/lists/* # Download, build and install libyang. Note that APT package is outdated @@ -37,23 +37,11 @@ RUN ldconfig # Set Python to show logs as they occur ENV PYTHONUNBUFFERED=0 -# Download the gRPC health probe -RUN GRPC_HEALTH_PROBE_VERSION=v0.2.0 && \ - wget -qO/bin/grpc_health_probe https://github.com/grpc-ecosystem/grpc-health-probe/releases/download/${GRPC_HEALTH_PROBE_VERSION}/grpc_health_probe-linux-amd64 && \ - chmod +x /bin/grpc_health_probe - # Get generic Python packages RUN python3 -m pip install --upgrade pip RUN python3 -m pip install --upgrade setuptools wheel RUN python3 -m pip install --upgrade pip-tools -# Get common Python packages -# Note: this step enables sharing the previous Docker build steps among all the Python components -WORKDIR /var/teraflow -COPY common_requirements.in common_requirements.in -RUN pip-compile --quiet --output-file=common_requirements.txt common_requirements.in -RUN python3 -m pip install -r common_requirements.txt - # Add common files into working directory WORKDIR /var/teraflow/common COPY src/common/. ./ @@ -94,4 +82,5 @@ RUN mkdir -p /var/teraflow/tests/tools COPY src/tests/tools/mock_osm/. tests/tools/mock_osm/ # Start the service -ENTRYPOINT ["python", "-m", "nbi.service"] +#ENTRYPOINT ["gunicorn", "-w", "4", "--worker-class", "eventlet", "-b", "0.0.0.0:8080", "nbi.service:nbi_app"] +ENTRYPOINT ["gunicorn", "-w", "4", "--worker-class", "geventwebsocket.gunicorn.workers.GeventWebSocketWorker", "-b", "0.0.0.0:8080", "nbi.service.app:nbi_app"] diff --git a/src/nbi/client/NbiClient.py b/src/nbi/client/NbiClient.py deleted file mode 100644 index 043035c5a4b0612652b1473ac6deeaa9e3cfce19..0000000000000000000000000000000000000000 --- a/src/nbi/client/NbiClient.py +++ /dev/null @@ -1,96 +0,0 @@ -# Copyright 2022-2024 ETSI SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/) -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import grpc, logging -from common.Constants import ServiceNameEnum -from common.Settings import get_service_host, get_service_port_grpc -from common.proto.nbi_pb2_grpc import NbiServiceStub -from common.proto.context_pb2 import ( - AuthenticationResult, Empty, Service, ServiceId, ServiceIdList, ServiceStatus, TeraFlowController) -from common.tools.client.RetryDecorator import retry, delay_exponential -from common.tools.grpc.Tools import grpc_message_to_json_string - -LOGGER = logging.getLogger(__name__) -MAX_RETRIES = 15 -DELAY_FUNCTION = delay_exponential(initial=0.01, increment=2.0, maximum=5.0) -RETRY_DECORATOR = retry(max_retries=MAX_RETRIES, delay_function=DELAY_FUNCTION, prepare_method_name='connect') - -class NbiClient: - def __init__(self, host=None, port=None): - if not host: host = get_service_host(ServiceNameEnum.NBI) - if not port: port = get_service_port_grpc(ServiceNameEnum.NBI) - self.endpoint = '{:s}:{:s}'.format(str(host), str(port)) - LOGGER.debug('Creating channel to {:s}...'.format(str(self.endpoint))) - self.channel = None - self.stub = None - self.connect() - LOGGER.debug('Channel created') - - def connect(self): - self.channel = grpc.insecure_channel(self.endpoint) - self.stub = NbiServiceStub(self.channel) - - def close(self): - if self.channel is not None: self.channel.close() - self.channel = None - self.stub = None - - @RETRY_DECORATOR - def CheckCredentials(self, request : TeraFlowController) -> AuthenticationResult: - LOGGER.debug('CheckCredentials request: {:s}'.format(grpc_message_to_json_string(request))) - response = self.stub.CheckCredentials(request) - LOGGER.debug('CheckCredentials result: {:s}'.format(grpc_message_to_json_string(response))) - return response - - @RETRY_DECORATOR - def GetConnectivityServiceStatus(self, request : ServiceId) -> ServiceStatus: - LOGGER.debug('GetConnectivityServiceStatus request: {:s}'.format(grpc_message_to_json_string(request))) - response = self.stub.GetConnectivityServiceStatus(request) - LOGGER.debug('GetConnectivityServiceStatus result: {:s}'.format(grpc_message_to_json_string(response))) - return response - - @RETRY_DECORATOR - def CreateConnectivityService(self, request : Service) -> ServiceId: - LOGGER.debug('CreateConnectivityService request: {:s}'.format(grpc_message_to_json_string(request))) - response = self.stub.CreateConnectivityService(request) - LOGGER.debug('CreateConnectivityService result: {:s}'.format(grpc_message_to_json_string(response))) - return response - - @RETRY_DECORATOR - def EditConnectivityService(self, request : Service) -> ServiceId: - LOGGER.debug('EditConnectivityService request: {:s}'.format(grpc_message_to_json_string(request))) - response = self.stub.EditConnectivityService(request) - LOGGER.debug('EditConnectivityService result: {:s}'.format(grpc_message_to_json_string(response))) - return response - - @RETRY_DECORATOR - def DeleteConnectivityService(self, request : Service) -> Empty: - LOGGER.debug('DeleteConnectivityService request: {:s}'.format(grpc_message_to_json_string(request))) - response = self.stub.DeleteConnectivityService(request) - LOGGER.debug('DeleteConnectivityService result: {:s}'.format(grpc_message_to_json_string(response))) - return response - - @RETRY_DECORATOR - def GetAllActiveConnectivityServices(self, request : Empty) -> ServiceIdList: - LOGGER.debug('GetAllActiveConnectivityServices request: {:s}'.format(grpc_message_to_json_string(request))) - response = self.stub.GetAllActiveConnectivityServices(request) - LOGGER.debug('GetAllActiveConnectivityServices result: {:s}'.format(grpc_message_to_json_string(response))) - return response - - @RETRY_DECORATOR - def ClearAllConnectivityServices(self, request : Empty) -> Empty: - LOGGER.debug('ClearAllConnectivityServices request: {:s}'.format(grpc_message_to_json_string(request))) - response = self.stub.ClearAllConnectivityServices(request) - LOGGER.debug('ClearAllConnectivityServices result: {:s}'.format(grpc_message_to_json_string(response))) - return response diff --git a/src/nbi/requirements.in b/src/nbi/requirements.in index d56ee821b19894e435368cd6250b25d3bdc33c10..431dbf2d2a869aaea4b2f20ca91c3ad6d1a8f7e5 100644 --- a/src/nbi/requirements.in +++ b/src/nbi/requirements.in @@ -14,10 +14,14 @@ deepdiff==6.7.* deepmerge==1.1.* +eventlet Flask==2.1.3 Flask-HTTPAuth==4.5.0 Flask-RESTful==0.3.9 +flask-socketio jsonschema==4.4.0 +gevent +gunicorn libyang==2.8.4 netaddr==0.9.0 pyang==2.6.0 @@ -25,4 +29,19 @@ git+https://github.com/robshakir/pyangbind.git pydantic==2.6.3 requests==2.27.1 werkzeug==2.3.7 -websockets==12.0 +#websockets==12.0 + +# from common_requirements; take required ones +#coverage==6.3 +#grpcio==1.47.* +#grpcio-health-checking==1.47.* +#grpcio-reflection==1.47.* +#grpcio-tools==1.47.* +#grpclib==0.4.4 +#prettytable==3.5.0 +#prometheus-client==0.13.0 +#protobuf==3.20.* +#pytest==6.2.5 +#pytest-benchmark==3.4.1 +#python-dateutil==2.8.2 +#pytest-depends==1.0.1 diff --git a/src/nbi/service/NbiApplication.py b/src/nbi/service/NbiApplication.py new file mode 100644 index 0000000000000000000000000000000000000000..5c2c00eac47765e83faadbf4f167978f83db2e7f --- /dev/null +++ b/src/nbi/service/NbiApplication.py @@ -0,0 +1,63 @@ +import logging, time +from typing import Any, Optional +from flask import Flask, request +from flask_restful import Api, Resource +from flask_socketio import Namespace, SocketIO + + +LOGGER = logging.getLogger(__name__) + +def log_request(response): + timestamp = time.strftime('[%Y-%b-%d %H:%M]') + LOGGER.info( + '%s %s %s %s %s', timestamp, request.remote_addr, request.method, + request.full_path, response.status + ) + return response + +class NbiApplication: + def __init__(self, base_url : Optional[str] = None) -> None: + if base_url is None: base_url = '' + self.base_url = base_url + + self.app = Flask(__name__) + self.app.after_request(log_request) + self.api = Api(self.app, prefix=base_url) + #websocket_path = '/'.join([base_url.rstrip('/'), 'websocket']) + self.sio = SocketIO(self.app, path=base_url, cors_allowed_origins="*") + + def add_rest_api_resource(self, resource_class : Resource, *urls, **kwargs) -> None: + self.api.add_resource(resource_class, *urls, **kwargs) + + def add_websocket_namespace(self, namespace_class : Namespace, namespace_url : str) -> None: + self.sio.on_namespace(namespace_class(namespace=namespace_url)) + + def websocket_emit_message( + self, event : str, *args : Any, namespace : str = "/", to : Optional[str] = None + ) -> None: + self.sio.emit(event, *args, namespace=namespace, to=to) + + def dump_configuration(self) -> None: + LOGGER.debug('Configured Resources:') + for resource in self.api.resources: + LOGGER.debug(' - {:s}'.format(str(resource))) + + LOGGER.debug('Configured Rules:') + for rule in self.app.url_map.iter_rules(): + LOGGER.debug(' - {:s}'.format(str(rule))) + + def run_standalone( + self, bind_address : Optional[str] = None, bind_port : Optional[int] = None + ) -> None: + # Run method used when started in a standalone mode, i.e., outside gunicorn or + # similar WSGI HTTP servers. Otherwise, use mechanism defined by the used + # WSGI HTTP server. + + #logging.getLogger('werkzeug').setLevel(logging.WARNING) + + endpoint = 'http://{:s}:{:s}'.format(str(bind_address), str(bind_port)) + if self.base_url is not None: + endpoint = '/'.join([endpoint.rstrip('/'), self.base_url]) + + LOGGER.info('Listening on {:s}...'.format(endpoint)) + self.sio.run(self.app, host=bind_address, port=bind_port) diff --git a/src/nbi/service/NbiService.py b/src/nbi/service/NbiService.py deleted file mode 100644 index fe7bf2e7b22271ea27746084ebee831257aa43d0..0000000000000000000000000000000000000000 --- a/src/nbi/service/NbiService.py +++ /dev/null @@ -1,28 +0,0 @@ -# Copyright 2022-2024 ETSI SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/) -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from common.Constants import ServiceNameEnum -from common.Settings import get_service_port_grpc -from common.proto.nbi_pb2_grpc import add_NbiServiceServicer_to_server -from common.tools.service.GenericGrpcService import GenericGrpcService -from nbi.service.NbiServiceServicerImpl import NbiServiceServicerImpl - -class NbiService(GenericGrpcService): - def __init__(self, cls_name: str = __name__) -> None: - port = get_service_port_grpc(ServiceNameEnum.NBI) - super().__init__(port, cls_name=cls_name) - self.nbi_servicer = NbiServiceServicerImpl() - - def install_servicers(self): - add_NbiServiceServicer_to_server(self.nbi_servicer, self.server) diff --git a/src/nbi/service/NbiServiceServicerImpl.py b/src/nbi/service/NbiServiceServicerImpl.py deleted file mode 100644 index 5719b67e4bb7a151d1f668132c7a8dc7073d4948..0000000000000000000000000000000000000000 --- a/src/nbi/service/NbiServiceServicerImpl.py +++ /dev/null @@ -1,63 +0,0 @@ -# Copyright 2022-2024 ETSI SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/) -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import grpc, logging -from common.method_wrappers.Decorator import MetricsPool, safe_and_metered_rpc_method -from common.proto.context_pb2 import ( - AuthenticationResult, Empty, Service, ServiceId, ServiceIdList, ServiceStatus, TeraFlowController) -from common.proto.nbi_pb2_grpc import NbiServiceServicer - -LOGGER = logging.getLogger(__name__) - -METRICS_POOL = MetricsPool('NBI', 'RPC') - -class NbiServiceServicerImpl(NbiServiceServicer): - def __init__(self): - LOGGER.info('Creating Servicer...') - LOGGER.info('Servicer Created') - - @safe_and_metered_rpc_method(METRICS_POOL, LOGGER) - def CheckCredentials(self, request : TeraFlowController, context : grpc.ServicerContext) -> AuthenticationResult: - LOGGER.warning('NOT IMPLEMENTED') - return AuthenticationResult() - - @safe_and_metered_rpc_method(METRICS_POOL, LOGGER) - def GetConnectivityServiceStatus(self, request : ServiceId, context : grpc.ServicerContext) -> ServiceStatus: - LOGGER.warning('NOT IMPLEMENTED') - return ServiceStatus() - - @safe_and_metered_rpc_method(METRICS_POOL, LOGGER) - def CreateConnectivityService(self, request : Service, context : grpc.ServicerContext) -> ServiceId: - LOGGER.warning('NOT IMPLEMENTED') - return ServiceId() - - @safe_and_metered_rpc_method(METRICS_POOL, LOGGER) - def EditConnectivityService(self, request : Service, context : grpc.ServicerContext) -> ServiceId: - LOGGER.warning('NOT IMPLEMENTED') - return ServiceId() - - @safe_and_metered_rpc_method(METRICS_POOL, LOGGER) - def DeleteConnectivityService(self, request : Service, context : grpc.ServicerContext) -> Empty: - LOGGER.warning('NOT IMPLEMENTED') - return Empty() - - @safe_and_metered_rpc_method(METRICS_POOL, LOGGER) - def GetAllActiveConnectivityServices(self, request : Empty, context : grpc.ServicerContext) -> ServiceIdList: - LOGGER.warning('NOT IMPLEMENTED') - return ServiceIdList() - - @safe_and_metered_rpc_method(METRICS_POOL, LOGGER) - def ClearAllConnectivityServices(self, request : Empty, context : grpc.ServicerContext) -> Empty: - LOGGER.warning('NOT IMPLEMENTED') - return Empty() diff --git a/src/nbi/service/__main__.py b/src/nbi/service/app.py similarity index 53% rename from src/nbi/service/__main__.py rename to src/nbi/service/app.py index 1d470f4eac30795e2272c9145baf947f3c982ba5..023f1500bdcfb167e4881329a4df0d34fb98a716 100644 --- a/src/nbi/service/__main__.py +++ b/src/nbi/service/app.py @@ -12,16 +12,17 @@ # See the License for the specific language governing permissions and # limitations under the License. -import logging, signal, sys, threading +import logging +from typing import Optional from prometheus_client import start_http_server from common.Constants import ServiceNameEnum from common.Settings import ( ENVVAR_SUFIX_SERVICE_HOST, ENVVAR_SUFIX_SERVICE_PORT_GRPC, - get_env_var_name, get_log_level, get_metrics_port, + get_env_var_name, get_http_bind_address, get_log_level, + get_metrics_port, get_service_baseurl_http, get_service_port_http, wait_for_environment_variables ) -from .NbiService import NbiService -from .rest_server.RestServer import RestServer +from .NbiApplication import NbiApplication from .rest_server.nbi_plugins.etsi_bwm import register_etsi_bwm_api from .rest_server.nbi_plugins.ietf_hardware import register_ietf_hardware from .rest_server.nbi_plugins.ietf_l2vpn import register_ietf_l2vpn @@ -32,22 +33,34 @@ from .rest_server.nbi_plugins.ietf_acl import register_ietf_acl from .rest_server.nbi_plugins.qkd_app import register_qkd_app from .rest_server.nbi_plugins.tfs_api import register_tfs_api from .rest_server.nbi_plugins import register_restconf -from .context_subscription import register_context_subscription - -terminate = threading.Event() -LOGGER = None - -def signal_handler(signal, frame): # pylint: disable=redefined-outer-name, unused-argument - LOGGER.warning('Terminate signal received') - terminate.set() - -def main(): - global LOGGER # pylint: disable=global-statement - - log_level = get_log_level() - logging.basicConfig(level=log_level) - LOGGER = logging.getLogger(__name__) - +from .websocket_namespaces.hearthbeat import register_heartbeat + + +LOG_LEVEL = get_log_level() +logging.basicConfig(level=LOG_LEVEL) +LOGGER = logging.getLogger(__name__) + +BIND_ADDRESS = get_http_bind_address() +BIND_PORT = get_service_port_http(ServiceNameEnum.NBI) +BASE_URL = get_service_baseurl_http(ServiceNameEnum.NBI) or '' + +REGISTER_METHODS = [ + register_etsi_bwm_api, + register_ietf_hardware, + register_ietf_l2vpn, + register_ietf_l3vpn, + register_ietf_network, + register_ietf_nss, + register_ietf_acl, + register_qkd_app, + register_tfs_api, + register_restconf, + register_heartbeat, +] + +def configure_nbi( + base_url : Optional[str] = None +) -> NbiApplication: wait_for_environment_variables([ get_env_var_name(ServiceNameEnum.CONTEXT, ENVVAR_SUFIX_SERVICE_HOST ), get_env_var_name(ServiceNameEnum.CONTEXT, ENVVAR_SUFIX_SERVICE_PORT_GRPC), @@ -57,52 +70,25 @@ def main(): get_env_var_name(ServiceNameEnum.SERVICE, ENVVAR_SUFIX_SERVICE_PORT_GRPC), ]) - signal.signal(signal.SIGINT, signal_handler) - signal.signal(signal.SIGTERM, signal_handler) - LOGGER.info('Starting...') # Start metrics server metrics_port = get_metrics_port() start_http_server(metrics_port) - # Starting NBI service - grpc_service = NbiService() - grpc_service.start() - - rest_server = RestServer() - register_etsi_bwm_api(rest_server) - register_ietf_hardware(rest_server) - register_ietf_l2vpn(rest_server) # Registering L2VPN entrypoint - register_ietf_l3vpn(rest_server) # Registering L3VPN entrypoint - register_ietf_network(rest_server) - register_ietf_nss(rest_server) # Registering NSS entrypoint - register_ietf_acl(rest_server) - register_qkd_app(rest_server) - register_tfs_api(rest_server) - register_restconf(rest_server) - rest_server.start() - - register_context_subscription() - - LOGGER.debug('Configured Resources:') - for resource in rest_server.api.resources: - LOGGER.debug(' - {:s}'.format(str(resource))) - - LOGGER.debug('Configured Rules:') - for rule in rest_server.app.url_map.iter_rules(): - LOGGER.debug(' - {:s}'.format(str(rule))) + nbi_app = NbiApplication(base_url=base_url) - # Wait for Ctrl+C or termination signal - while not terminate.wait(timeout=1.0): pass + for register_method in REGISTER_METHODS: + register_method(nbi_app) - LOGGER.info('Terminating...') - grpc_service.stop() - rest_server.shutdown() - rest_server.join() + nbi_app.dump_configuration() - LOGGER.info('Bye') - return 0 + return nbi_app if __name__ == '__main__': - sys.exit(main()) + # Only used to run it locally during development stage; + # otherwise, app is directly launched by gunicorn. + _nbi_app = configure_nbi(base_url=BASE_URL) + _nbi_app.run_standalone( + bind_address=BIND_ADDRESS, bind_port=BIND_PORT + ) diff --git a/src/nbi/client/__init__.py b/src/nbi/service/websocket_namespaces/__init__.py similarity index 100% rename from src/nbi/client/__init__.py rename to src/nbi/service/websocket_namespaces/__init__.py diff --git a/src/nbi/service/websocket_namespaces/hearthbeat/HeartbeatNamespace.py b/src/nbi/service/websocket_namespaces/hearthbeat/HeartbeatNamespace.py new file mode 100644 index 0000000000000000000000000000000000000000..b9b8b0be2d63e6da79ce8ca658993f78bb8566d5 --- /dev/null +++ b/src/nbi/service/websocket_namespaces/hearthbeat/HeartbeatNamespace.py @@ -0,0 +1,41 @@ +import logging, threading, time +from flask import request +from flask_socketio import Namespace, join_room, leave_room +from nbi.service.NbiApplication import NbiApplication + +LOGGER = logging.getLogger(__name__) + +NAMESPACE_NAME = 'heartbeat' +NAMESPACE_URL = '/heartbeat' + +# WebSocket Heartbeat Namespace for debugging purposes +class DebugHeartbeatHandler(Namespace): + def on_connect(self): + LOGGER.debug('Client {:s} connected'.format(str(request.sid))) + join_room(NAMESPACE_NAME) + + def on_disconnect(self, reason): + LOGGER.debug('Client {:s} disconnected: reason={:s}'.format( + str(request.sid), str(reason) + )) + leave_room(NAMESPACE_NAME) + +class DebugHeartbeatThread(threading.Thread): + INTERVAL = 1 # second + + def __init__(self, nbi_app : NbiApplication): + super().__init__(daemon=True) + self.nbi_app = nbi_app + + def run(self): + interval = DebugHeartbeatThread.INTERVAL + start_time = time.time() + while True: + time.sleep(interval) + uptime = time.time() - start_time + self.nbi_app.websocket_emit_message( + 'uptime', {'uptime_seconds': uptime}, + namespace=NAMESPACE_URL, to=NAMESPACE_NAME + ) + +NAMESPACE_DESCRIPTOR = (NAMESPACE_NAME, DebugHeartbeatHandler, NAMESPACE_URL) diff --git a/src/nbi/service/rest_server/RestServer.py b/src/nbi/service/websocket_namespaces/hearthbeat/__init__.py similarity index 56% rename from src/nbi/service/rest_server/RestServer.py rename to src/nbi/service/websocket_namespaces/hearthbeat/__init__.py index 521de2735f4d8476c59a263651a67f42851084e2..435a425b4848813a450f00eaa746b9a3e29b6609 100644 --- a/src/nbi/service/rest_server/RestServer.py +++ b/src/nbi/service/websocket_namespaces/hearthbeat/__init__.py @@ -12,12 +12,10 @@ # See the License for the specific language governing permissions and # limitations under the License. -from common.Constants import ServiceNameEnum -from common.Settings import get_service_baseurl_http, get_service_port_http -from common.tools.service.GenericRestServer import GenericRestServer -class RestServer(GenericRestServer): - def __init__(self, cls_name: str = __name__) -> None: - bind_port = get_service_port_http(ServiceNameEnum.NBI) - base_url = get_service_baseurl_http(ServiceNameEnum.NBI) - super().__init__(bind_port, base_url, cls_name=cls_name) +from nbi.service.NbiApplication import NbiApplication +from .HeartbeatNamespace import NAMESPACE_DESCRIPTOR + +def register_heartbeat(nbi_app : NbiApplication): + _, namespace_class, namespace_url = NAMESPACE_DESCRIPTOR + nbi_app.add_websocket_namespace(namespace_class, namespace_url) diff --git a/src/webui/service/__init__.py b/src/webui/service/__init__.py index f64ecca3b6453baf193c683c11fbb2ca27629286..d33e2118e61e2eb2be8bb17afa1c10fa822d348a 100644 --- a/src/webui/service/__init__.py +++ b/src/webui/service/__init__.py @@ -137,6 +137,6 @@ def create_app(use_config=None, web_app_root=None): 'is_deployed_slice' : is_deployed_slice, }) - if web_app_root is not None: + if web_app_root is not None and len(web_app_root) > 0: app.wsgi_app = SetSubAppMiddleware(app.wsgi_app, web_app_root) return app