diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 1e35c2cb390fdc44a4d3f1da0ffdfc32e5d96c07..c8f78f4dfdebb51bfa985485f404b124a4c4ae60 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -13,6 +13,7 @@ include: - local: '/manifests/.gitlab-ci.yml' - local: '/src/monitoring/.gitlab-ci.yml' #- local: '/src/centralizedattackdetector/.gitlab-ci.yml' + - local: '/src/compute/.gitlab-ci.yml' - local: '/src/context/.gitlab-ci.yml' - local: '/src/device/.gitlab-ci.yml' - local: '/src/service/.gitlab-ci.yml' diff --git a/manifests/computeservice.yaml b/manifests/computeservice.yaml index cdca52eb890d832cfb56457b89d44a045ee4af57..73380f75daeb7d60891c3d093ca7651ba4280e58 100644 --- a/manifests/computeservice.yaml +++ b/manifests/computeservice.yaml @@ -17,6 +17,7 @@ spec: image: registry.gitlab.com/teraflow-h2020/controller/compute:latest imagePullPolicy: Always ports: + - containerPort: 8080 - containerPort: 9090 env: - name: LOG_LEVEL @@ -44,7 +45,12 @@ spec: selector: app: computeservice ports: + - name: http + protocol: TCP + port: 8080 + targetPort: 8080 - name: grpc + protocol: TCP port: 9090 targetPort: 9090 --- @@ -59,6 +65,10 @@ spec: selector: app: computeservice ports: + - name: http + protocol: TCP + port: 8080 + targetPort: 8080 - name: grpc protocol: TCP port: 9090 diff --git a/manifests/contextservice.yaml b/manifests/contextservice.yaml index c0ea046e0ae1d9e4bb6b6a70de5a8b26844981fc..7ccf3e4f0dc9abb41f228d4fb7c4ec18fad93954 100644 --- a/manifests/contextservice.yaml +++ b/manifests/contextservice.yaml @@ -34,6 +34,8 @@ spec: env: - name: DB_BACKEND value: "redis" + - name: MB_BACKEND + value: "redis" - name: REDIS_DATABASE_ID value: "0" - name: LOG_LEVEL @@ -64,9 +66,11 @@ spec: app: contextservice ports: - name: grpc + protocol: TCP port: 1010 targetPort: 1010 - name: http + protocol: TCP port: 8080 targetPort: 8080 --- diff --git a/src/common/type_checkers/Checkers.py b/src/common/type_checkers/Checkers.py index d0eddcf213143c4c3d99c9edfafd1305384e777b..f78395c9c5f480bf75b4c9344dfeb3b48f9da062 100644 --- a/src/common/type_checkers/Checkers.py +++ b/src/common/type_checkers/Checkers.py @@ -1,5 +1,5 @@ import re -from typing import Any, Container, List, Optional, Pattern, Set, Sized, Tuple, Union +from typing import Any, Container, Dict, List, Optional, Pattern, Set, Sized, Tuple, Union def chk_none(name : str, value : Any, reason=None) -> Any: if value is None: return value @@ -11,6 +11,11 @@ def chk_not_none(name : str, value : Any, reason=None) -> Any: if reason is None: reason = 'must not be None.' raise ValueError('{}({}) {}'.format(str(name), str(value), str(reason))) +def chk_attribute(name : str, container : Dict, container_name : str, **kwargs): + if name in container: return container[name] + if 'default' in kwargs: return kwargs['default'] + raise AttributeError('Missing object({:s}) in container({:s})'.format(str(name), str(container_name))) + def chk_type(name : str, value : Any, type_or_types : Union[type, Set[type]] = set()) -> Any: if isinstance(value, type_or_types): return value msg = '{}({}) is of a wrong type({}). Accepted type_or_types({}).' diff --git a/src/compute/.gitlab-ci.yml b/src/compute/.gitlab-ci.yml index 948092ea3b896ce5e487bd7b9e42a6aff6743626..d10d5acfbcafa90a3fa6c0bd2cad9164e67dc020 100644 --- a/src/compute/.gitlab-ci.yml +++ b/src/compute/.gitlab-ci.yml @@ -10,8 +10,8 @@ build compute: - docker build -t "$IMAGE_NAME:$IMAGE_TAG" -f ./src/$IMAGE_NAME/Dockerfile ./src/ - docker tag "$IMAGE_NAME:$IMAGE_TAG" "$CI_REGISTRY_IMAGE/$IMAGE_NAME:$IMAGE_TAG" - docker push "$CI_REGISTRY_IMAGE/$IMAGE_NAME:$IMAGE_TAG" -# after_script: -# - docker rmi $(docker images --quiet --filter=dangling=true) + after_script: + - docker images --filter="dangling=true" --quiet | xargs -r docker rmi rules: - if: '$CI_PIPELINE_SOURCE == "merge_request_event" && ($CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "develop" || $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == $CI_DEFAULT_BRANCH)' - if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "develop"' @@ -36,9 +36,10 @@ unit test compute: - if docker container ls | grep $IMAGE_NAME; then docker rm -f $IMAGE_NAME; else echo "$IMAGE_NAME image is not in the system"; fi script: - docker pull "$CI_REGISTRY_IMAGE/$IMAGE_NAME:$IMAGE_TAG" - - docker run --name $IMAGE_NAME -d -p 9090:9090 --network=teraflowbridge --rm $CI_REGISTRY_IMAGE/$IMAGE_NAME:$IMAGE_TAG + - docker run --name $IMAGE_NAME -d -p 9090:9090 --network=teraflowbridge $CI_REGISTRY_IMAGE/$IMAGE_NAME:$IMAGE_TAG - sleep 5 - docker ps -a + - docker logs $IMAGE_NAME - docker exec -i $IMAGE_NAME bash -c "pytest --log-level=DEBUG --verbose $IMAGE_NAME/tests/test_unitary.py" after_script: - docker rm -f $IMAGE_NAME diff --git a/src/compute/Config.py b/src/compute/Config.py index e95740493fd16940cee2d1e780472a1e90801303..b2d3179fab6e55368ba751aac48de507551c4516 100644 --- a/src/compute/Config.py +++ b/src/compute/Config.py @@ -1,4 +1,5 @@ import logging +from werkzeug.security import generate_password_hash # General settings LOG_LEVEL = logging.WARNING @@ -10,7 +11,10 @@ GRPC_GRACE_PERIOD = 60 # REST-API settings RESTAPI_SERVICE_PORT = 8080 -RESTAPI_BASE_URL = '/api' +RESTAPI_BASE_URL = '/restconf/data' +RESTAPI_USERS = { # TODO: implement a database of credentials and permissions + 'admin': generate_password_hash('admin'), +} # Prometheus settings METRICS_PORT = 9192 diff --git a/src/compute/Dockerfile b/src/compute/Dockerfile index 83e4fad35704dd8febb8aa627936d54c02e89f68..99d4e3ed1adc8a688874f3291f4891112543b3ff 100644 --- a/src/compute/Dockerfile +++ b/src/compute/Dockerfile @@ -30,6 +30,8 @@ RUN python3 -m pip install -r compute/requirements.in # Add files into working directory COPY common/. common COPY compute/. compute +COPY context/. context +COPY service/. service # Start compute service ENTRYPOINT ["python", "-m", "compute.service"] diff --git a/src/compute/requirements.in b/src/compute/requirements.in index 1da334a54b1c42b01eb8f731d8fd5bd975edd2cf..42a56f905a011aec0df5c21c9e3b8196d7896395 100644 --- a/src/compute/requirements.in +++ b/src/compute/requirements.in @@ -1,5 +1,10 @@ +Flask +Flask-HTTPAuth +Flask-RESTful grpcio-health-checking grpcio +jsonschema prometheus-client pytest pytest-benchmark +requests diff --git a/src/compute/service/ComputeService.py b/src/compute/service/ComputeService.py index 36d43283c43c82faff1748428943cb9a9687c840..51a15472bc83416ebbfc3c421e5cfed2a9682bdc 100644 --- a/src/compute/service/ComputeService.py +++ b/src/compute/service/ComputeService.py @@ -24,9 +24,9 @@ class ComputeService: self.server = None def start(self): - self.endpoint = '{}:{}'.format(self.address, self.port) - LOGGER.debug('Starting Service (tentative endpoint: {}, max_workers: {})...'.format( - self.endpoint, self.max_workers)) + self.endpoint = '{:s}:{:s}'.format(str(self.address), str(self.port)) + LOGGER.debug('Starting Service (tentative endpoint: {:s}, max_workers: {:s})...'.format( + str(self.endpoint), str(self.max_workers))) self.pool = futures.ThreadPoolExecutor(max_workers=self.max_workers) self.server = grpc.server(self.pool) # , interceptors=(tracer_interceptor,)) @@ -39,15 +39,15 @@ class ComputeService: add_HealthServicer_to_server(self.health_servicer, self.server) port = self.server.add_insecure_port(self.endpoint) - self.endpoint = '{}:{}'.format(self.address, port) - LOGGER.info('Listening on {}...'.format(self.endpoint)) + self.endpoint = '{:s}:{:s}'.format(str(self.address), str(port)) + LOGGER.info('Listening on {:s}...'.format(str(self.endpoint))) self.server.start() self.health_servicer.set(OVERALL_HEALTH, HealthCheckResponse.SERVING) # pylint: disable=maybe-no-member LOGGER.debug('Service started') def stop(self): - LOGGER.debug('Stopping service (grace period {} seconds)...'.format(self.grace_period)) + LOGGER.debug('Stopping service (grace period {:s} seconds)...'.format(str(self.grace_period))) self.health_servicer.enter_graceful_shutdown() self.server.stop(self.grace_period) LOGGER.debug('Service stopped') diff --git a/src/compute/service/__main__.py b/src/compute/service/__main__.py index f45af374c471222bb4fdb089860418c5895d6321..eacc1f6c464112192194fca5827033aedc57385c 100644 --- a/src/compute/service/__main__.py +++ b/src/compute/service/__main__.py @@ -4,9 +4,9 @@ from common.Settings import get_setting from compute.Config import ( GRPC_SERVICE_PORT, GRPC_MAX_WORKERS, GRPC_GRACE_PERIOD, LOG_LEVEL, RESTAPI_SERVICE_PORT, RESTAPI_BASE_URL, METRICS_PORT) -from compute.service.ComputeService import ComputeService -from compute.service.rest_server.Server import Server -from compute.service.rest_server.resources.Compute import Compute +from .ComputeService import ComputeService +from .rest_server.RestServer import RestServer +from .rest_server.nbi_plugins.ietf_l2vpn import register_ietf_l2vpn terminate = threading.Event() LOGGER = None @@ -41,9 +41,8 @@ def main(): grpc_service = ComputeService(port=grpc_service_port, max_workers=max_workers, grace_period=grace_period) grpc_service.start() - rest_server = Server(port=restapi_service_port, base_url=restapi_base_url) - rest_server.add_resource( - Compute, '/restconf/config/compute', endpoint='api.compute') + rest_server = RestServer(port=restapi_service_port, base_url=restapi_base_url) + register_ietf_l2vpn(rest_server) rest_server.start() # Wait for Ctrl+C or termination signal diff --git a/src/compute/service/rest_server/Server.py b/src/compute/service/rest_server/RestServer.py similarity index 66% rename from src/compute/service/rest_server/Server.py rename to src/compute/service/rest_server/RestServer.py index c68515e915a6de82b3b08525d3383ac21b6c25b2..8ed8dbbbf69bc89c9c76fdf31e16b0687d47856e 100644 --- a/src/compute/service/rest_server/Server.py +++ b/src/compute/service/rest_server/RestServer.py @@ -1,6 +1,6 @@ -import logging, threading -from flask import Flask -from flask_restful import Api +import logging, threading, time +from flask import Flask, request +from flask_restful import Api, Resource from werkzeug.serving import make_server from compute.Config import RESTAPI_BASE_URL, RESTAPI_SERVICE_PORT @@ -9,16 +9,24 @@ logging.getLogger('werkzeug').setLevel(logging.WARNING) BIND_ADDRESS = '0.0.0.0' LOGGER = logging.getLogger(__name__) -class Server(threading.Thread): +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 RestServer(threading.Thread): def __init__(self, host=BIND_ADDRESS, port=RESTAPI_SERVICE_PORT, base_url=RESTAPI_BASE_URL): threading.Thread.__init__(self, daemon=True) self.host = host self.port = port self.base_url = base_url + self.srv = None + self.ctx = None self.app = Flask(__name__) + self.app.after_request(log_request) self.api = Api(self.app, prefix=self.base_url) - def add_resource(self, resource, *urls, **kwargs): + def add_resource(self, resource : Resource, *urls, **kwargs): self.api.add_resource(resource, *urls, **kwargs) def run(self): diff --git a/src/compute/service/rest_server/resources/__init__.py b/src/compute/service/rest_server/nbi_plugins/__init__.py similarity index 100% rename from src/compute/service/rest_server/resources/__init__.py rename to src/compute/service/rest_server/nbi_plugins/__init__.py diff --git a/src/compute/service/rest_server/nbi_plugins/ietf_l2vpn/Constants.py b/src/compute/service/rest_server/nbi_plugins/ietf_l2vpn/Constants.py new file mode 100644 index 0000000000000000000000000000000000000000..87c32c444d39acb048ede9105c9a0dc2c7e3899e --- /dev/null +++ b/src/compute/service/rest_server/nbi_plugins/ietf_l2vpn/Constants.py @@ -0,0 +1,3 @@ +DEFAULT_MTU = 1512 +DEFAULT_ADDRESS_FAMILIES = ['IPV4'] +DEFAULT_SUB_INTERFACE_INDEX = 0 diff --git a/src/compute/service/rest_server/nbi_plugins/ietf_l2vpn/L2VPN_Service.py b/src/compute/service/rest_server/nbi_plugins/ietf_l2vpn/L2VPN_Service.py new file mode 100644 index 0000000000000000000000000000000000000000..752a027ad0d41f67f6a2312ee166a51ebcbc23bd --- /dev/null +++ b/src/compute/service/rest_server/nbi_plugins/ietf_l2vpn/L2VPN_Service.py @@ -0,0 +1,69 @@ +import logging +from typing import Dict, List +from flask import request +from flask.json import jsonify +from flask_restful import Resource +from werkzeug.exceptions import UnsupportedMediaType +from common.Constants import DEFAULT_CONTEXT_UUID +from common.Settings import get_setting +from context.client.ContextClient import ContextClient +from context.proto.context_pb2 import ServiceId +from service.client.ServiceClient import ServiceClient +from service.proto.context_pb2 import Service, ServiceStatusEnum, ServiceTypeEnum +from .tools.Authentication import HTTP_AUTH +from .tools.HttpStatusCodes import HTTP_CREATED, HTTP_GATEWAYTIMEOUT, HTTP_NOCONTENT, HTTP_OK, HTTP_SERVERERROR + +LOGGER = logging.getLogger(__name__) + +class L2VPN_Service(Resource): + def __init__(self) -> None: + super().__init__() + self.context_client = ContextClient( + get_setting('CONTEXTSERVICE_SERVICE_HOST'), get_setting('CONTEXTSERVICE_SERVICE_PORT_GRPC')) + self.service_client = ServiceClient( + get_setting('SERVICESERVICE_SERVICE_HOST'), get_setting('SERVICESERVICE_SERVICE_PORT_GRPC')) + + @HTTP_AUTH.login_required + def get(self, vpn_id : str): + LOGGER.debug('VPN_Id: {:s}'.format(str(vpn_id))) + LOGGER.debug('Request: {:s}'.format(str(request))) + + # pylint: disable=no-member + service_id_request = ServiceId() + service_id_request.context_id.context_uuid.uuid = DEFAULT_CONTEXT_UUID + service_id_request.service_uuid.uuid = vpn_id + + try: + service_reply = self.context_client.GetService(service_id_request) + if service_reply.service_id != service_id_request: # pylint: disable=no-member + raise Exception('Service retrieval failed. Wrong Service Id was returned') + + service_ready_status = ServiceStatusEnum.SERVICESTATUS_ACTIVE + service_status = service_reply.service_status.service_status + response = jsonify({}) + response.status_code = HTTP_OK if service_status == service_ready_status else HTTP_GATEWAYTIMEOUT + except Exception as e: # pylint: disable=broad-except + LOGGER.exception('Something went wrong Retrieving Service {:s}'.format(str(request))) + response = jsonify({'error': str(e)}) + response.status_code = HTTP_SERVERERROR + return response + + @HTTP_AUTH.login_required + def delete(self, vpn_id : str): + LOGGER.debug('VPN_Id: {:s}'.format(str(vpn_id))) + LOGGER.debug('Request: {:s}'.format(str(request))) + + # pylint: disable=no-member + service_id_request = ServiceId() + service_id_request.context_id.context_uuid.uuid = DEFAULT_CONTEXT_UUID + service_id_request.service_uuid.uuid = vpn_id + + try: + self.service_client.DeleteService(service_id_request) + response = jsonify({}) + response.status_code = HTTP_NOCONTENT + except Exception as e: # pylint: disable=broad-except + LOGGER.exception('Something went wrong Deleting Service {:s}'.format(str(request))) + response = jsonify({'error': str(e)}) + response.status_code = HTTP_SERVERERROR + return response diff --git a/src/compute/service/rest_server/nbi_plugins/ietf_l2vpn/L2VPN_Services.py b/src/compute/service/rest_server/nbi_plugins/ietf_l2vpn/L2VPN_Services.py new file mode 100644 index 0000000000000000000000000000000000000000..2ed0293f0729c6d4617a445034702f706a6daa25 --- /dev/null +++ b/src/compute/service/rest_server/nbi_plugins/ietf_l2vpn/L2VPN_Services.py @@ -0,0 +1,55 @@ +import logging +from typing import Dict, List +from flask import request +from flask.json import jsonify +from flask_restful import Resource +from werkzeug.exceptions import UnsupportedMediaType +from common.Constants import DEFAULT_CONTEXT_UUID +from common.Settings import get_setting +from service.client.ServiceClient import ServiceClient +from service.proto.context_pb2 import Service, ServiceStatusEnum, ServiceTypeEnum +from .schemas.vpn_service import SCHEMA_VPN_SERVICE +from .tools.Authentication import HTTP_AUTH +from .tools.HttpStatusCodes import HTTP_CREATED, HTTP_SERVERERROR +from .tools.Validator import validate_message + +LOGGER = logging.getLogger(__name__) + +class L2VPN_Services(Resource): + def __init__(self) -> None: + super().__init__() + self.service_client = ServiceClient( + get_setting('SERVICESERVICE_SERVICE_HOST'), get_setting('SERVICESERVICE_SERVICE_PORT_GRPC')) + + @HTTP_AUTH.login_required + def get(self): + return {} + + @HTTP_AUTH.login_required + def post(self): + if not request.is_json: raise UnsupportedMediaType('JSON payload is required') + request_data : Dict = request.json + LOGGER.debug('Request: {:s}'.format(str(request_data))) + validate_message(SCHEMA_VPN_SERVICE, request_data) + + vpn_services : List[Dict] = request_data['ietf-l2vpn-svc:vpn-service'] + for vpn_service in vpn_services: + # pylint: disable=no-member + service_request = Service() + service_request.service_id.context_id.context_uuid.uuid = DEFAULT_CONTEXT_UUID + service_request.service_id.service_uuid.uuid = vpn_service['vpn-id'] + service_request.service_type = ServiceTypeEnum.SERVICETYPE_L3NM + service_request.service_status.service_status = ServiceStatusEnum.SERVICESTATUS_PLANNED + + try: + service_reply = self.service_client.CreateService(service_request) + if service_reply != service_request.service_id: # pylint: disable=no-member + raise Exception('Service creation failed. Wrong Service Id was returned') + + response = jsonify({}) + response.status_code = HTTP_CREATED + except Exception as e: # pylint: disable=broad-except + LOGGER.exception('Something went wrong Creating Service {:s}'.format(str(request))) + response = jsonify({'error': str(e)}) + response.status_code = HTTP_SERVERERROR + return response diff --git a/src/compute/service/rest_server/nbi_plugins/ietf_l2vpn/L2VPN_SiteNetworkAccesses.py b/src/compute/service/rest_server/nbi_plugins/ietf_l2vpn/L2VPN_SiteNetworkAccesses.py new file mode 100644 index 0000000000000000000000000000000000000000..639e8c63f5a2e64b166ab976632fd83437673484 --- /dev/null +++ b/src/compute/service/rest_server/nbi_plugins/ietf_l2vpn/L2VPN_SiteNetworkAccesses.py @@ -0,0 +1,161 @@ +import json, logging +from typing import Dict +from flask import request +from flask.json import jsonify +from flask.wrappers import Response +from flask_restful import Resource +from werkzeug.exceptions import UnsupportedMediaType +from common.Constants import DEFAULT_CONTEXT_UUID +from common.Settings import get_setting +from context.client.ContextClient import ContextClient +from context.proto.context_pb2 import Service, ServiceId, ServiceStatusEnum +from service.client.ServiceClient import ServiceClient +from .schemas.site_network_access import SCHEMA_SITE_NETWORK_ACCESS +from .tools.Authentication import HTTP_AUTH +from .tools.HttpStatusCodes import HTTP_NOCONTENT, HTTP_SERVERERROR +from .tools.Validator import validate_message +from .Constants import DEFAULT_ADDRESS_FAMILIES, DEFAULT_MTU, DEFAULT_SUB_INTERFACE_INDEX + +LOGGER = logging.getLogger(__name__) + +def process_site_network_access(context_client : ContextClient, site_network_access : Dict) -> Service: + vpn_id = site_network_access['vpn-attachment']['vpn-id'] + cvlan_id = site_network_access['connection']['tagged-interface']['dot1q-vlan-tagged']['cvlan-id'] + bearer_reference = site_network_access['bearer']['bearer-reference'] + + # Assume bearer_reference = '<device_uuid>:<endpoint_uuid>:<router_id>' + # Assume route_distinguisher = 0:<cvlan_id> + device_uuid,endpoint_uuid,router_id = bearer_reference.split(':') + route_distinguisher = '0:{:d}'.format(cvlan_id) + + # pylint: disable=no-member + service_id = ServiceId() + service_id.context_id.context_uuid.uuid = DEFAULT_CONTEXT_UUID + service_id.service_uuid.uuid = vpn_id + + service_readonly = context_client.GetService(service_id) + service = Service() + service.CopyFrom(service_readonly) + + for endpoint_id in service.service_endpoint_ids: # pylint: disable=no-member + if endpoint_id.device_id.device_uuid.uuid != device_uuid: continue + if endpoint_id.endpoint_uuid.uuid != endpoint_uuid: continue + break # found, do nothing + else: + # not found, add it + endpoint_id = service.service_endpoint_ids.add() # pylint: disable=no-member + endpoint_id.device_id.device_uuid.uuid = device_uuid + endpoint_id.endpoint_uuid.uuid = endpoint_uuid + + for config_rule in service.service_config.config_rules: # pylint: disable=no-member + if config_rule.resource_key != 'settings': continue + json_settings = json.loads(config_rule.resource_value) + + if 'route_distinguisher' not in json_settings: # missing, add it + json_settings['route_distinguisher'] = route_distinguisher + elif json_settings['route_distinguisher'] != route_distinguisher: # differs, raise exception + msg = 'Specified RouteDistinguisher({:s}) differs from Service RouteDistinguisher({:s})' + raise Exception(msg.format(str(json_settings['route_distinguisher']), str(route_distinguisher))) + + if 'mtu' not in json_settings: # missing, add it + json_settings['mtu'] = DEFAULT_MTU + elif json_settings['mtu'] != DEFAULT_MTU: # differs, raise exception + msg = 'Specified MTU({:s}) differs from Service MTU({:s})' + raise Exception(msg.format(str(json_settings['mtu']), str(DEFAULT_MTU))) + + if 'address_families' not in json_settings: # missing, add it + json_settings['address_families'] = DEFAULT_ADDRESS_FAMILIES + elif json_settings['address_families'] != DEFAULT_ADDRESS_FAMILIES: # differs, raise exception + msg = 'Specified AddressFamilies({:s}) differs from Service AddressFamilies({:s})' + raise Exception(msg.format(str(json_settings['address_families']), str(DEFAULT_ADDRESS_FAMILIES))) + + config_rule.resource_value = json.dumps(json_settings, sort_keys=True) + break + else: + # not found, add it + config_rule = service.service_config.config_rules.add() # pylint: disable=no-member + config_rule.resource_key = 'settings' + config_rule.resource_value = json.dumps({ + 'route_distinguisher': route_distinguisher, + 'mtu': DEFAULT_MTU, + 'address_families': DEFAULT_ADDRESS_FAMILIES, + }, sort_keys=True) + + endpoint_settings_key = 'device[{:s}]/endpoint[{:s}]/settings'.format(device_uuid, endpoint_uuid) + for config_rule in service.service_config.config_rules: # pylint: disable=no-member + if config_rule.resource_key != endpoint_settings_key: continue + json_settings = json.loads(config_rule.resource_value) + + if 'router_id' not in json_settings: # missing, add it + json_settings['router_id'] = router_id + elif json_settings['router_id'] != router_id: # differs, raise exception + msg = 'Specified RouterId({:s}) differs from Service RouterId({:s})' + raise Exception(msg.format(str(json_settings['router_id']), str(router_id))) + + if 'sub_interface_index' not in json_settings: # missing, add it + json_settings['sub_interface_index'] = DEFAULT_SUB_INTERFACE_INDEX + elif json_settings['sub_interface_index'] != DEFAULT_SUB_INTERFACE_INDEX: # differs, raise exception + msg = 'Specified SubInterfaceIndex({:s}) differs from Service SubInterfaceIndex({:s})' + raise Exception(msg.format( + str(json_settings['sub_interface_index']), str(DEFAULT_SUB_INTERFACE_INDEX))) + + config_rule.resource_value = json.dumps(json_settings, sort_keys=True) + break + else: + # not found, add it + config_rule = service.service_config.config_rules.add() # pylint: disable=no-member + config_rule.resource_key = endpoint_settings_key + config_rule.resource_value = json.dumps({ + 'router_id': router_id, + 'sub_interface_index': DEFAULT_SUB_INTERFACE_INDEX, + }, sort_keys=True) + + if len(service.service_endpoint_ids) >= 2: + service.service_status.service_status = ServiceStatusEnum.SERVICESTATUS_ACTIVE + + return service + +def process_list_site_network_access( + context_client : ContextClient, service_client : ServiceClient, request_data : Dict) -> Response: + + LOGGER.debug('Request: {:s}'.format(str(request_data))) + validate_message(SCHEMA_SITE_NETWORK_ACCESS, request_data) + + errors = [] + for site_network_access in request_data['ietf-l2vpn-svc:site-network-access']: + try: + service_request = process_site_network_access(context_client, site_network_access) + service_reply = service_client.CreateService(service_request) + if service_reply != service_request.service_id: # pylint: disable=no-member + raise Exception('Service update failed. Wrong Service Id was returned') + except Exception as e: # pylint: disable=broad-except + LOGGER.exception('Something went wrong Updating Service {:s}'.format(str(request))) + errors.append({'error': str(e)}) + + response = jsonify(errors) + response.status_code = HTTP_NOCONTENT if len(errors) == 0 else HTTP_SERVERERROR + return response + +class L2VPN_SiteNetworkAccesses(Resource): + def __init__(self) -> None: + super().__init__() + self.context_client = ContextClient( + get_setting('CONTEXTSERVICE_SERVICE_HOST'), get_setting('CONTEXTSERVICE_SERVICE_PORT_GRPC')) + self.service_client = ServiceClient( + get_setting('SERVICESERVICE_SERVICE_HOST'), get_setting('SERVICESERVICE_SERVICE_PORT_GRPC')) + + #@HTTP_AUTH.login_required + #def get(self): + # return {} + + @HTTP_AUTH.login_required + def post(self, site_id : str): + if not request.is_json: raise UnsupportedMediaType('JSON payload is required') + LOGGER.debug('Site_Id: {:s}'.format(str(site_id))) + return process_list_site_network_access(self.context_client, self.service_client, request.json) + + @HTTP_AUTH.login_required + def put(self, site_id : str): + if not request.is_json: raise UnsupportedMediaType('JSON payload is required') + LOGGER.debug('Site_Id: {:s}'.format(str(site_id))) + return process_list_site_network_access(self.context_client, self.service_client, request.json) diff --git a/src/compute/service/rest_server/nbi_plugins/ietf_l2vpn/__init__.py b/src/compute/service/rest_server/nbi_plugins/ietf_l2vpn/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..979c8a3bc1903381516bf0f9683bbe4e4f2c3cb3 --- /dev/null +++ b/src/compute/service/rest_server/nbi_plugins/ietf_l2vpn/__init__.py @@ -0,0 +1,22 @@ +# RFC 8466 - L2VPN Service Model (L2SM) +# Ref: https://datatracker.ietf.org/doc/html/rfc8466 + +from flask_restful import Resource +from compute.service.rest_server.RestServer import RestServer +from .L2VPN_Services import L2VPN_Services +from .L2VPN_Service import L2VPN_Service +from .L2VPN_SiteNetworkAccesses import L2VPN_SiteNetworkAccesses + +URL_PREFIX = '/ietf-l2vpn-svc:l2vpn-svc' + +def _add_resource(rest_server : RestServer, resource : Resource, *urls, **kwargs): + urls = [(URL_PREFIX + url) for url in urls] + rest_server.add_resource(resource, *urls, **kwargs) + +def register_ietf_l2vpn(rest_server : RestServer): + _add_resource(rest_server, L2VPN_Services, + '/vpn-services') + _add_resource(rest_server, L2VPN_Service, + '/vpn-services/vpn-service=<vpn_id>', '/vpn-services/vpn-service=<vpn_id>/') + _add_resource(rest_server, L2VPN_SiteNetworkAccesses, + '/sites/site=<site_id>/site-network-accesses', '/sites/site=<site_id>/site-network-accesses/') diff --git a/src/compute/service/rest_server/nbi_plugins/ietf_l2vpn/schemas/Common.py b/src/compute/service/rest_server/nbi_plugins/ietf_l2vpn/schemas/Common.py new file mode 100644 index 0000000000000000000000000000000000000000..f54da792b526cede52b94892ee9946fb63c6b015 --- /dev/null +++ b/src/compute/service/rest_server/nbi_plugins/ietf_l2vpn/schemas/Common.py @@ -0,0 +1,2 @@ +# String pattern for UUIDs such as '3fd942ee-2dc3-41d1-aeec-65aa85d117b2' +REGEX_UUID = r'[a-fA-F0-9]{8}\-[a-fA-F0-9]{4}\-[a-fA-F0-9]{4}\-[a-fA-F0-9]{4}\-[a-fA-F0-9]{12}' diff --git a/src/compute/service/rest_server/nbi_plugins/ietf_l2vpn/schemas/__init__.py b/src/compute/service/rest_server/nbi_plugins/ietf_l2vpn/schemas/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/compute/service/rest_server/nbi_plugins/ietf_l2vpn/schemas/site_network_access.py b/src/compute/service/rest_server/nbi_plugins/ietf_l2vpn/schemas/site_network_access.py new file mode 100644 index 0000000000000000000000000000000000000000..33ba8cc7fe5be76f82fbd74cd3608703f37e76a0 --- /dev/null +++ b/src/compute/service/rest_server/nbi_plugins/ietf_l2vpn/schemas/site_network_access.py @@ -0,0 +1,66 @@ +# Example request: +# request = {'ietf-l2vpn-svc:site-network-access': [{ +# 'network-access-id': '3fd942ee-2dc3-41d1-aeec-65aa85d117b2', +# 'vpn-attachment': {'vpn-id': '954b1b53-4a8c-406d-9eff-750ec2c9a258', +# 'site-role': 'any-to-any-role'}, +# 'connection': {'encapsulation-type': 'dot1q-vlan-tagged', 'tagged-interface': { +# 'dot1q-vlan-tagged': {'cvlan-id': 1234}}}, +# 'bearer': {'bearer-reference': '1a'} +# }]} + +from .Common import REGEX_UUID + +SCHEMA_SITE_NETWORK_ACCESS = { + '$schema': 'https://json-schema.org/draft/2020-12/schema', + 'type': 'object', + 'required': ['ietf-l2vpn-svc:site-network-access'], + 'properties': { + 'ietf-l2vpn-svc:site-network-access': { + 'type': 'array', + 'minItems': 1, + 'maxItems': 1, # by now we do not support multiple site-network-access in the same message + 'items': { + 'type': 'object', + 'required': ['network-access-id', 'vpn-attachment', 'connection', 'bearer'], + 'properties': { + 'network-access-id': {'type': 'string', 'pattern': REGEX_UUID}, + 'vpn-attachment': { + 'type': 'object', + 'required': ['vpn-id', 'site-role'], + 'properties': { + 'vpn-id': {'type': 'string', 'pattern': REGEX_UUID}, + 'site-role': {'type': 'string', 'minLength': 1}, + }, + }, + 'connection': { + 'type': 'object', + 'required': ['encapsulation-type', 'tagged-interface'], + 'properties': { + 'encapsulation-type': {'enum': ['dot1q-vlan-tagged']}, + 'tagged-interface': { + 'type': 'object', + 'required': ['dot1q-vlan-tagged'], + 'properties': { + 'dot1q-vlan-tagged': { + 'type': 'object', + 'required': ['cvlan-id'], + 'properties': { + 'cvlan-id': {'type': 'integer', 'minimum': 1, 'maximum': 4094}, + }, + }, + }, + }, + }, + }, + 'bearer': { + 'type': 'object', + 'required': ['bearer-reference'], + 'properties': { + 'bearer-reference': {'type': 'string', 'minLength': 1}, + }, + }, + }, + }, + }, + }, +} diff --git a/src/compute/service/rest_server/nbi_plugins/ietf_l2vpn/schemas/vpn_service.py b/src/compute/service/rest_server/nbi_plugins/ietf_l2vpn/schemas/vpn_service.py new file mode 100644 index 0000000000000000000000000000000000000000..54e9c53163b8d764a37b613501f6b427d6e1773d --- /dev/null +++ b/src/compute/service/rest_server/nbi_plugins/ietf_l2vpn/schemas/vpn_service.py @@ -0,0 +1,32 @@ +# Example request: +# request = {'ietf-l2vpn-svc:vpn-service': [{ +# 'vpn-id': 'c6270231-f1de-4687-b2ed-7b58f9105775', +# 'vpn-svc-type': 'vpws', +# 'svc-topo': 'any-to-any', +# 'customer-name': 'osm' +# }]} + +from .Common import REGEX_UUID + +SCHEMA_VPN_SERVICE = { + '$schema': 'https://json-schema.org/draft/2020-12/schema', + 'type': 'object', + 'required': ['ietf-l2vpn-svc:vpn-service'], + 'properties': { + 'ietf-l2vpn-svc:vpn-service': { + 'type': 'array', + 'minItems': 1, + 'maxItems': 1, # by now we do not support multiple vpn-service in the same message + 'items': { + 'type': 'object', + 'required': ['vpn-id', 'vpn-svc-type', 'svc-topo', 'customer-name'], + 'properties': { + 'vpn-id': {'type': 'string', 'pattern': REGEX_UUID}, + 'vpn-svc-type': {'enum': ['vpws']}, + 'svc-topo': {'enum': ['any-to-any']}, + 'customer-name': {'const': 'osm'}, + }, + } + } + }, +} diff --git a/src/compute/service/rest_server/nbi_plugins/ietf_l2vpn/tools/Authentication.py b/src/compute/service/rest_server/nbi_plugins/ietf_l2vpn/tools/Authentication.py new file mode 100644 index 0000000000000000000000000000000000000000..de7c9eafd7b2d5afdc39b82a4d02bea20127fa4a --- /dev/null +++ b/src/compute/service/rest_server/nbi_plugins/ietf_l2vpn/tools/Authentication.py @@ -0,0 +1,11 @@ +from flask_httpauth import HTTPBasicAuth +from werkzeug.security import check_password_hash +from compute.Config import RESTAPI_USERS + +HTTP_AUTH = HTTPBasicAuth() + +@HTTP_AUTH.verify_password +def verify_password(username, password): + if username not in RESTAPI_USERS: return None + if not check_password_hash(RESTAPI_USERS[username], password): return None + return username diff --git a/src/compute/service/rest_server/nbi_plugins/ietf_l2vpn/tools/HttpStatusCodes.py b/src/compute/service/rest_server/nbi_plugins/ietf_l2vpn/tools/HttpStatusCodes.py new file mode 100644 index 0000000000000000000000000000000000000000..5879670102e861bf1598104ace80f1f0cdb931ca --- /dev/null +++ b/src/compute/service/rest_server/nbi_plugins/ietf_l2vpn/tools/HttpStatusCodes.py @@ -0,0 +1,6 @@ +HTTP_OK = 200 +HTTP_CREATED = 201 +HTTP_NOCONTENT = 204 +HTTP_BADREQUEST = 400 +HTTP_SERVERERROR = 500 +HTTP_GATEWAYTIMEOUT = 504 \ No newline at end of file diff --git a/src/compute/service/rest_server/nbi_plugins/ietf_l2vpn/tools/Validator.py b/src/compute/service/rest_server/nbi_plugins/ietf_l2vpn/tools/Validator.py new file mode 100644 index 0000000000000000000000000000000000000000..9c126d71beba72ebb7b69d9852927cb31ac2a614 --- /dev/null +++ b/src/compute/service/rest_server/nbi_plugins/ietf_l2vpn/tools/Validator.py @@ -0,0 +1,21 @@ +from typing import List +from flask.json import jsonify +from jsonschema import _utils +from jsonschema.validators import validator_for +from jsonschema.protocols import Validator +from jsonschema.exceptions import ValidationError +from werkzeug.exceptions import BadRequest +from .HttpStatusCodes import HTTP_BADREQUEST + +def validate_message(schema, message): + validator_class = validator_for(schema) + validator : Validator = validator_class(schema) + errors : List[ValidationError] = sorted(validator.iter_errors(message), key=str) + if len(errors) == 0: return + response = jsonify([ + {'message': str(error.message), 'schema': str(error.schema), 'validator': str(error.validator), + 'where': str(_utils.format_as_index(container='message', indices=error.relative_path))} + for error in errors + ]) + response.status_code = HTTP_BADREQUEST + raise BadRequest(response=response) diff --git a/src/compute/service/rest_server/nbi_plugins/ietf_l2vpn/tools/__init__.py b/src/compute/service/rest_server/nbi_plugins/ietf_l2vpn/tools/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/compute/service/rest_server/resources/Compute.py b/src/compute/service/rest_server/resources/Compute.py deleted file mode 100644 index 4b845be2edd20c512bd0669739d402207d71fa94..0000000000000000000000000000000000000000 --- a/src/compute/service/rest_server/resources/Compute.py +++ /dev/null @@ -1,52 +0,0 @@ -import logging -from flask.json import jsonify -from flask_restful import Resource -from common.Settings import get_setting -from common.Constants import DEFAULT_CONTEXT_UUID -from service.client.ServiceClient import ServiceClient -from service.proto.context_pb2 import Service, ServiceStatusEnum, ServiceTypeEnum - -LOGGER = logging.getLogger(__name__) - -class Compute(Resource): - def __init__(self) -> None: - super().__init__() - - def get(self): - # Here implement HTTP GET method - raise NotImplementedError() - - def post(self): - # Here implement HTTP POST method - - # Retrieve required data from request - new_service_context_id = DEFAULT_CONTEXT_UUID - new_service_id = 'my-service-id' - - # Find Service address/port from environment and instantiate client - service_host = get_setting('SERVICESERVICE_SERVICE_HOST') - service_port = get_setting('SERVICESERVICE_SERVICE_PORT_GRPC') - service_client = ServiceClient(service_host, service_port) - - # Compose a dummy CreateService request - request = Service() - request.service_id.context_id.context_uuid.uuid = new_service_context_id - request.service_id.service_uuid.uuid = new_service_id - request.service_type = ServiceTypeEnum.SERVICETYPE_L2NM - request.service_status.service_status = ServiceStatusEnum.SERVICESTATUS_PLANNED - - try: - # Issue gRPC request to Service component - reply = service_client.CreateService(request) - - # Parse CreateService reply, here we check that obtained service Id and context are the expected ones. - reply_context_uuid = reply.context_id.context_uuid.uuid - reply_service_uuid = reply.service_uuid.uuid - #succeeded = (reply_context_uuid == new_service_context_id) and (reply_service_uuid == new_service_id) - succeeded = True - reply = {'succeeded': succeeded} - except Exception as e: - LOGGER.exception('Something went wrong Creating Service {:s}'.format(str(request))) - reply = {'succeeded': False, 'error': str(e)} - - return jsonify(reply) diff --git a/src/compute/tests/MockService.py b/src/compute/tests/MockService.py new file mode 100644 index 0000000000000000000000000000000000000000..54b420f5aa1cf015c90f09b874f9b37225e07328 --- /dev/null +++ b/src/compute/tests/MockService.py @@ -0,0 +1,41 @@ +import grpc, logging +from concurrent import futures + +GRPC_MAX_WORKERS = 10 +GRPC_GRACE_PERIOD = 60 + +class MockService: + def __init__(self, address, port, max_workers=GRPC_MAX_WORKERS, grace_period=GRPC_GRACE_PERIOD, cls_name=__name__): + self.logger = logging.getLogger(cls_name) + self.address = address + self.port = port + self.endpoint = None + self.max_workers = max_workers + self.grace_period = grace_period + self.pool = None + self.server = None + + def install_servicers(self): + pass + + def start(self): + self.endpoint = '{:s}:{:s}'.format(str(self.address), str(self.port)) + self.logger.info('Starting Service (tentative endpoint: {:s}, max_workers: {:s})...'.format( + str(self.endpoint), str(self.max_workers))) + + self.pool = futures.ThreadPoolExecutor(max_workers=self.max_workers) + self.server = grpc.server(self.pool) # , interceptors=(tracer_interceptor,)) + + self.install_servicers() + + port = self.server.add_insecure_port(self.endpoint) + self.endpoint = '{:s}:{:s}'.format(str(self.address), str(port)) + self.logger.info('Listening on {:s}...'.format(str(self.endpoint))) + self.server.start() + + self.logger.debug('Service started') + + def stop(self): + self.logger.debug('Stopping service (grace period {:s} seconds)...'.format(str(self.grace_period))) + self.server.stop(self.grace_period) + self.logger.debug('Service stopped') diff --git a/src/compute/tests/MockServicerImpl_Context.py b/src/compute/tests/MockServicerImpl_Context.py new file mode 100644 index 0000000000000000000000000000000000000000..d79a755d49773dff4b298abdba6dfa38d9e69d57 --- /dev/null +++ b/src/compute/tests/MockServicerImpl_Context.py @@ -0,0 +1,188 @@ +import grpc, logging +from typing import Any, Dict, Iterator, List +from context.proto.context_pb2 import ( + Context, ContextEvent, ContextId, ContextIdList, ContextList, Device, DeviceEvent, DeviceId, DeviceIdList, + DeviceList, Empty, Link, LinkEvent, LinkId, LinkIdList, LinkList, Service, ServiceEvent, ServiceId, ServiceIdList, + ServiceList, Topology, TopologyEvent, TopologyId, TopologyIdList, TopologyList) +from context.proto.context_pb2_grpc import ContextServiceServicer +from .Tools import grpc_message_to_json_string + +LOGGER = logging.getLogger(__name__) + +def get_container(database : Dict[str, Dict[str, Any]], container_name : str) -> Dict[str, Any]: + return database.setdefault(container_name, {}) + +def get_entries(database : Dict[str, Dict[str, Any]], container_name : str) -> List[Any]: + container = get_container(database, container_name) + return [container[entry_uuid] for entry_uuid in sorted(container.keys())] + +def get_entry( + context : grpc.ServicerContext, database : Dict[str, Dict[str, Any]], container_name : str, entry_uuid : str +) -> Any: + LOGGER.debug('[get_entry] AFTER database={:s}'.format(str(database))) + container = get_container(database, container_name) + if entry_uuid not in container: + context.abort(grpc.StatusCode.INTERNAL, str('{:s}({:s}) not found'.format(container_name, entry_uuid))) + return container[entry_uuid] + +def set_entry(database : Dict[str, Dict[str, Any]], container_name : str, entry_uuid : str, entry : Any) -> Any: + container = get_container(database, container_name) + LOGGER.debug('[set_entry] BEFORE database={:s}'.format(str(database))) + container[entry_uuid] = entry + LOGGER.debug('[set_entry] AFTER database={:s}'.format(str(database))) + return entry + +def del_entry( + context : grpc.ServicerContext, database : Dict[str, Dict[str, Any]], container_name : str, entry_uuid : str +) -> Any: + container = get_container(database, container_name) + if entry_uuid not in container: + context.abort(grpc.StatusCode.INTERNAL, str('{:s}({:s}) not found'.format(container_name, entry_uuid))) + del container[entry_uuid] + return Empty() + +class MockServicerImpl_Context(ContextServiceServicer): + def __init__(self): + LOGGER.info('[__init__] Creating Servicer...') + self.database : Dict[str, Any] = {} + LOGGER.info('[__init__] Servicer Created') + + # ----- Context ---------------------------------------------------------------------------------------------------- + + def ListContextIds(self, request: Empty, context : grpc.ServicerContext) -> ContextIdList: + LOGGER.info('[ListContextIds] request={:s}'.format(grpc_message_to_json_string(request))) + return ContextIdList(context_ids=[context.context_id for context in get_entries(self.database, 'context')]) + + def ListContexts(self, request: Empty, context : grpc.ServicerContext) -> ContextList: + LOGGER.info('[ListContexts] request={:s}'.format(grpc_message_to_json_string(request))) + return ContextList(contexts=get_entries(self.database, 'context')) + + def GetContext(self, request: ContextId, context : grpc.ServicerContext) -> Context: + LOGGER.info('[GetContext] request={:s}'.format(grpc_message_to_json_string(request))) + return get_entry(context, self.database, 'context', request.context_uuid.uuid) + + def SetContext(self, request: Context, context : grpc.ServicerContext) -> ContextId: + LOGGER.info('[SetContext] request={:s}'.format(grpc_message_to_json_string(request))) + return set_entry(self.database, 'context', request.context_uuid.uuid, request).context_id + + def RemoveContext(self, request: ContextId, context : grpc.ServicerContext) -> Empty: + LOGGER.info('[RemoveContext] request={:s}'.format(grpc_message_to_json_string(request))) + return del_entry(context, self.database, 'context', request.context_uuid.uuid) + + def GetContextEvents(self, request: Empty, context : grpc.ServicerContext) -> Iterator[ContextEvent]: + LOGGER.info('[GetContextEvents] request={:s}'.format(grpc_message_to_json_string(request))) + + + # ----- Topology --------------------------------------------------------------------------------------------------- + + def ListTopologyIds(self, request: ContextId, context : grpc.ServicerContext) -> TopologyIdList: + LOGGER.info('[ListTopologyIds] request={:s}'.format(grpc_message_to_json_string(request))) + topologies = get_entries(self.database, 'topology[{:s}]'.format(str(request.context_id.context_uuid.uuid))) + return TopologyIdList(topology_ids=[topology.topology_id for topology in topologies]) + + def ListTopologies(self, request: ContextId, context : grpc.ServicerContext) -> TopologyList: + LOGGER.info('[ListTopologies] request={:s}'.format(grpc_message_to_json_string(request))) + topologies = get_entries(self.database, 'topology[{:s}]'.format(str(request.context_id.context_uuid.uuid))) + return TopologyList(topologies=[topology for topology in topologies]) + + def GetTopology(self, request: TopologyId, context : grpc.ServicerContext) -> Topology: + LOGGER.info('[GetTopology] request={:s}'.format(grpc_message_to_json_string(request))) + container_name = 'topology[{:s}]'.format(str(request.context_id.context_uuid.uuid)) + return get_entry(context, self.database, container_name, request.topology_uuid.uuid) + + def SetTopology(self, request: Topology, context : grpc.ServicerContext) -> TopologyId: + LOGGER.info('[SetTopology] request={:s}'.format(grpc_message_to_json_string(request))) + container_name = 'topology[{:s}]'.format(str(request.topology_id.context_id.context_uuid.uuid)) + return set_entry(self.database, container_name, request.topology_id.topology_uuid.uuid, request).topology_id + + def RemoveTopology(self, request: TopologyId, context : grpc.ServicerContext) -> Empty: + LOGGER.info('[RemoveTopology] request={:s}'.format(grpc_message_to_json_string(request))) + container_name = 'topology[{:s}]'.format(str(request.context_id.context_uuid.uuid)) + return del_entry(context, self.database, container_name, request.topology_uuid.uuid) + + def GetTopologyEvents(self, request: Empty, context : grpc.ServicerContext) -> Iterator[TopologyEvent]: + LOGGER.info('[GetTopologyEvents] request={:s}'.format(grpc_message_to_json_string(request))) + + + # ----- Device ----------------------------------------------------------------------------------------------------- + + def ListDeviceIds(self, request: Empty, context : grpc.ServicerContext) -> DeviceIdList: + LOGGER.info('[ListDeviceIds] request={:s}'.format(grpc_message_to_json_string(request))) + return DeviceIdList(device_ids=[device.device_id for device in get_entries(self.database, 'device')]) + + def ListDevices(self, request: Empty, context : grpc.ServicerContext) -> DeviceList: + LOGGER.info('[ListDevices] request={:s}'.format(grpc_message_to_json_string(request))) + return DeviceList(devices=get_entries(self.database, 'device')) + + def GetDevice(self, request: DeviceId, context : grpc.ServicerContext) -> Device: + LOGGER.info('[GetDevice] request={:s}'.format(grpc_message_to_json_string(request))) + return get_entry(context, self.database, 'device', request.device_uuid.uuid) + + def SetDevice(self, request: Context, context : grpc.ServicerContext) -> DeviceId: + LOGGER.info('[SetDevice] request={:s}'.format(grpc_message_to_json_string(request))) + return set_entry(self.database, 'device', request.device_uuid.uuid, request).device_id + + def RemoveDevice(self, request: DeviceId, context : grpc.ServicerContext) -> Empty: + LOGGER.info('[RemoveDevice] request={:s}'.format(grpc_message_to_json_string(request))) + return del_entry(context, self.database, 'device', request.device_uuid.uuid) + + def GetDeviceEvents(self, request: Empty, context : grpc.ServicerContext) -> Iterator[DeviceEvent]: + LOGGER.info('[GetDeviceEvents] request={:s}'.format(grpc_message_to_json_string(request))) + + + # ----- Link ------------------------------------------------------------------------------------------------------- + + def ListLinkIds(self, request: Empty, context : grpc.ServicerContext) -> LinkIdList: + LOGGER.info('[ListLinkIds] request={:s}'.format(grpc_message_to_json_string(request))) + return LinkIdList(link_ids=[link.link_id for link in get_entries(self.database, 'link')]) + + def ListLinks(self, request: Empty, context : grpc.ServicerContext) -> LinkList: + LOGGER.info('[ListLinks] request={:s}'.format(grpc_message_to_json_string(request))) + return LinkList(links=get_entries(self.database, 'link')) + + def GetLink(self, request: LinkId, context : grpc.ServicerContext) -> Link: + LOGGER.info('[GetLink] request={:s}'.format(grpc_message_to_json_string(request))) + return get_entry(context, self.database, 'link', request.link_uuid.uuid) + + def SetLink(self, request: Context, context : grpc.ServicerContext) -> LinkId: + LOGGER.info('[SetLink] request={:s}'.format(grpc_message_to_json_string(request))) + return set_entry(self.database, 'link', request.link_uuid.uuid, request).link_id + + def RemoveLink(self, request: LinkId, context : grpc.ServicerContext) -> Empty: + LOGGER.info('[RemoveLink] request={:s}'.format(grpc_message_to_json_string(request))) + return del_entry(context, self.database, 'link', request.link_uuid.uuid) + + def GetLinkEvents(self, request: Empty, context : grpc.ServicerContext) -> Iterator[LinkEvent]: + LOGGER.info('[GetLinkEvents] request={:s}'.format(grpc_message_to_json_string(request))) + + + # ----- Service ---------------------------------------------------------------------------------------------------- + + def ListServiceIds(self, request: ContextId, context : grpc.ServicerContext) -> ServiceIdList: + LOGGER.info('[ListServiceIds] request={:s}'.format(grpc_message_to_json_string(request))) + services = get_entries(self.database, 'service[{:s}]'.format(str(request.context_id.context_uuid.uuid))) + return ServiceIdList(service_ids=[service.service_id for service in services]) + + def ListServices(self, request: ContextId, context : grpc.ServicerContext) -> ServiceList: + LOGGER.info('[ListServices] request={:s}'.format(grpc_message_to_json_string(request))) + services = get_entries(self.database, 'service[{:s}]'.format(str(request.context_id.context_uuid.uuid))) + return ServiceList(services=[service for service in services]) + + def GetService(self, request: ServiceId, context : grpc.ServicerContext) -> Service: + LOGGER.info('[GetService] request={:s}'.format(grpc_message_to_json_string(request))) + container_name = 'service[{:s}]'.format(str(request.context_id.context_uuid.uuid)) + return get_entry(context, self.database, container_name, request.service_uuid.uuid) + + def SetService(self, request: Service, context : grpc.ServicerContext) -> ServiceId: + LOGGER.info('[SetService] request={:s}'.format(grpc_message_to_json_string(request))) + return set_entry( + self.database, 'service[{:s}]'.format(str(request.service_id.context_id.context_uuid.uuid)), + request.service_id.service_uuid.uuid, request).service_id + + def RemoveService(self, request: ServiceId, context : grpc.ServicerContext) -> Empty: + LOGGER.info('[RemoveService] request={:s}'.format(grpc_message_to_json_string(request))) + container_name = 'service[{:s}]'.format(str(request.context_id.context_uuid.uuid)) + return del_entry(context, self.database, container_name, request.service_uuid.uuid) + + def GetServiceEvents(self, request: Empty, context : grpc.ServicerContext) -> Iterator[ServiceEvent]: + LOGGER.info('[GetServiceEvents] request={:s}'.format(grpc_message_to_json_string(request))) diff --git a/src/compute/tests/MockServicerImpl_Service.py b/src/compute/tests/MockServicerImpl_Service.py new file mode 100644 index 0000000000000000000000000000000000000000..75fdc3073dac1b942c7701f3a0be9feacb60109b --- /dev/null +++ b/src/compute/tests/MockServicerImpl_Service.py @@ -0,0 +1,32 @@ +import grpc, logging +from common.Settings import get_setting +from context.client.ContextClient import ContextClient +from service.proto.context_pb2 import ConnectionList, Empty, Service, ServiceId +from service.proto.service_pb2_grpc import ServiceServiceServicer +from .Tools import grpc_message_to_json_string + +LOGGER = logging.getLogger(__name__) + +class MockServicerImpl_Service(ServiceServiceServicer): + def __init__(self): + LOGGER.info('[__init__] Creating Servicer...') + self.context_client = ContextClient( + get_setting('CONTEXTSERVICE_SERVICE_HOST'), + get_setting('CONTEXTSERVICE_SERVICE_PORT_GRPC')) + LOGGER.info('[__init__] Servicer Created') + + def CreateService(self, request : Service, context : grpc.ServicerContext) -> ServiceId: + LOGGER.info('[CreateService] request={:s}'.format(grpc_message_to_json_string(request))) + return self.context_client.SetService(request) + + def UpdateService(self, request : Service, context : grpc.ServicerContext) -> ServiceId: + LOGGER.info('[UpdateService] request={:s}'.format(grpc_message_to_json_string(request))) + return self.context_client.SetService(request) + + def DeleteService(self, request : ServiceId, context : grpc.ServicerContext) -> Empty: + LOGGER.info('[DeleteService] request={:s}'.format(grpc_message_to_json_string(request))) + return self.context_client.RemoveService(request) + + def GetConnectionList(self, request : ServiceId, context : grpc.ServicerContext) -> ConnectionList: + LOGGER.info('[GetConnectionList] request={:s}'.format(grpc_message_to_json_string(request))) + return ConnectionList() diff --git a/src/compute/tests/Tools.py b/src/compute/tests/Tools.py new file mode 100644 index 0000000000000000000000000000000000000000..a96c38ce546d4062df8229f5506f9dd49af6fc81 --- /dev/null +++ b/src/compute/tests/Tools.py @@ -0,0 +1,7 @@ +import json +from google.protobuf.json_format import MessageToDict + +def grpc_message_to_json_string(message): + return json.dumps(MessageToDict( + message, including_default_value_fields=True, preserving_proto_field_name=True, use_integers_for_enums=False), + sort_keys=True) diff --git a/src/compute/tests/mock_osm/MockOSM.py b/src/compute/tests/mock_osm/MockOSM.py new file mode 100644 index 0000000000000000000000000000000000000000..c50ee6c88e75a62a743bba065830ae82827fa7d7 --- /dev/null +++ b/src/compute/tests/mock_osm/MockOSM.py @@ -0,0 +1,94 @@ +import logging +from .WimconnectorIETFL2VPN import WimconnectorIETFL2VPN + +LOGGER = logging.getLogger(__name__) + +WIM_USERNAME = 'admin' +WIM_PASSWORD = 'admin' + +# Ref: https://osm.etsi.org/wikipub/index.php/WIM +WIM_MAPPING = [ + { + 'device-id' : 'dev-1', # pop_switch_dpid + #'device_interface_id' : ??, # pop_switch_port + 'service_endpoint_id' : 'ep-1', # wan_service_endpoint_id + 'service_mapping_info': { # wan_service_mapping_info, other extra info + 'bearer': {'bearer-reference': 'dev-1:ep-1:10.0.0.1'}, + 'site-id': '1', + }, + #'switch_dpid' : ??, # wan_switch_dpid + #'switch_port' : ??, # wan_switch_port + #'datacenter_id' : ??, # vim_account + }, + { + 'device-id' : 'dev-2', # pop_switch_dpid + #'device_interface_id' : ??, # pop_switch_port + 'service_endpoint_id' : 'ep-2', # wan_service_endpoint_id + 'service_mapping_info': { # wan_service_mapping_info, other extra info + 'bearer': {'bearer-reference': 'dev-2:ep-2:10.0.0.2'}, + 'site-id': '2', + }, + #'switch_dpid' : ??, # wan_switch_dpid + #'switch_port' : ??, # wan_switch_port + #'datacenter_id' : ??, # vim_account + }, + { + 'device-id' : 'dev-3', # pop_switch_dpid + #'device_interface_id' : ??, # pop_switch_port + 'service_endpoint_id' : 'ep-3', # wan_service_endpoint_id + 'service_mapping_info': { # wan_service_mapping_info, other extra info + 'bearer': {'bearer-reference': 'dev-3:ep-3:10.0.0.3'}, + 'site-id': '3', + }, + #'switch_dpid' : ??, # wan_switch_dpid + #'switch_port' : ??, # wan_switch_port + #'datacenter_id' : ??, # vim_account + }, +] + +SERVICE_TYPE = 'ELINE' +SERVICE_CONNECTION_POINTS_1 = [ + {'service_endpoint_id': 'ep-1', + 'service_endpoint_encapsulation_type': 'dot1q', + 'service_endpoint_encapsulation_info': {'vlan': 1234}}, + {'service_endpoint_id': 'ep-2', + 'service_endpoint_encapsulation_type': 'dot1q', + 'service_endpoint_encapsulation_info': {'vlan': 1234}}, +] + +SERVICE_CONNECTION_POINTS_2 = [ + {'service_endpoint_id': 'ep-3', + 'service_endpoint_encapsulation_type': 'dot1q', + 'service_endpoint_encapsulation_info': {'vlan': 1234}}, +] + +class MockOSM: + def __init__(self, wim_url): + wim = {'wim_url': wim_url} + wim_account = {'user': WIM_USERNAME, 'password': WIM_PASSWORD} + config = {'mapping_not_needed': False, 'service_endpoint_mapping': WIM_MAPPING} + self.wim = WimconnectorIETFL2VPN(wim, wim_account, config=config) + self.service_uuid = None + self.conn_info = None + + def create_connectivity_service(self): + self.wim.check_credentials() + LOGGER.info('[create_connectivity_service] connection_points={:s}'.format(str(SERVICE_CONNECTION_POINTS_1))) + result = self.wim.create_connectivity_service(SERVICE_TYPE, SERVICE_CONNECTION_POINTS_1) + LOGGER.info('[create_connectivity_service] result={:s}'.format(str(result))) + self.service_uuid, self.conn_info = result + + def get_connectivity_service_status(self): + self.wim.check_credentials() + result = self.wim.get_connectivity_service_status(self.service_uuid, conn_info=self.conn_info) + LOGGER.info('[get_connectivity_service] result={:s}'.format(str(result))) + + def edit_connectivity_service(self): + self.wim.check_credentials() + LOGGER.info('[edit_connectivity_service] connection_points={:s}'.format(str(SERVICE_CONNECTION_POINTS_2))) + self.wim.edit_connectivity_service( + self.service_uuid, conn_info=self.conn_info, connection_points=SERVICE_CONNECTION_POINTS_2) + + def delete_connectivity_service(self): + self.wim.check_credentials() + self.wim.delete_connectivity_service(self.service_uuid, conn_info=self.conn_info) diff --git a/src/compute/tests/mock_osm/WimconnectorIETFL2VPN.py b/src/compute/tests/mock_osm/WimconnectorIETFL2VPN.py new file mode 100644 index 0000000000000000000000000000000000000000..182115bad67a4fbe1eb04a83ed8d54be964568c8 --- /dev/null +++ b/src/compute/tests/mock_osm/WimconnectorIETFL2VPN.py @@ -0,0 +1,499 @@ +# -*- coding: utf-8 -*- +## +# Copyright 2018 Telefonica +# All Rights Reserved. +# +# Contributors: Oscar Gonzalez de Dios, Manuel Lopez Bravo, Guillermo Pajares Martin +# 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. +# +# This work has been performed in the context of the Metro-Haul project - +# funded by the European Commission under Grant number 761727 through the +# Horizon 2020 program. +## +"""The SDN/WIM connector is responsible for establishing wide area network +connectivity. + +This SDN/WIM connector implements the standard IETF RFC 8466 "A YANG Data + Model for Layer 2 Virtual Private Network (L2VPN) Service Delivery" + +It receives the endpoints and the necessary details to request +the Layer 2 service. +""" +import requests +import uuid +import logging +#from osm_ro_plugin.sdnconn import SdnConnectorBase, SdnConnectorError +from .sdnconn import SdnConnectorBase, SdnConnectorError + +"""Check layer where we move it""" + + +class WimconnectorIETFL2VPN(SdnConnectorBase): + def __init__(self, wim, wim_account, config=None, logger=None): + """IETF L2VPN WIM connector + + Arguments: (To be completed) + wim (dict): WIM record, as stored in the database + wim_account (dict): WIM account record, as stored in the database + """ + self.logger = logging.getLogger("ro.sdn.ietfl2vpn") + super().__init__(wim, wim_account, config, logger) + self.headers = {"Content-Type": "application/json"} + self.mappings = { + m["service_endpoint_id"]: m for m in self.service_endpoint_mapping + } + self.user = wim_account.get("user") + self.passwd = wim_account.get("password") # replace "passwordd" -> "password" + + if self.user and self.passwd is not None: + self.auth = (self.user, self.passwd) + else: + self.auth = None + + self.logger.info("IETFL2VPN Connector Initialized.") + + def check_credentials(self): + endpoint = "{}/restconf/data/ietf-l2vpn-svc:l2vpn-svc/vpn-services".format( + self.wim["wim_url"] + ) + + try: + response = requests.get(endpoint, auth=self.auth) + http_code = response.status_code + except requests.exceptions.RequestException as e: + raise SdnConnectorError(e.message, http_code=503) + + if http_code != 200: + raise SdnConnectorError("Failed while authenticating", http_code=http_code) + + self.logger.info("Credentials checked") + + def get_connectivity_service_status(self, service_uuid, conn_info=None): + """Monitor the status of the connectivity service stablished + + Arguments: + service_uuid: Connectivity service unique identifier + + Returns: + Examples:: + {'sdn_status': 'ACTIVE'} + {'sdn_status': 'INACTIVE'} + {'sdn_status': 'DOWN'} + {'sdn_status': 'ERROR'} + """ + try: + self.logger.info("Sending get connectivity service stuatus") + servicepoint = "{}/restconf/data/ietf-l2vpn-svc:l2vpn-svc/vpn-services/vpn-service={}/".format( + self.wim["wim_url"], service_uuid + ) + response = requests.get(servicepoint, auth=self.auth) + + if response.status_code != requests.codes.ok: + raise SdnConnectorError( + "Unable to obtain connectivity servcice status", + http_code=response.status_code, + ) + + service_status = {"sdn_status": "ACTIVE"} + + return service_status + except requests.exceptions.ConnectionError: + raise SdnConnectorError("Request Timeout", http_code=408) + + def search_mapp(self, connection_point): + id = connection_point["service_endpoint_id"] + if id not in self.mappings: + raise SdnConnectorError("Endpoint {} not located".format(str(id))) + else: + return self.mappings[id] + + def create_connectivity_service(self, service_type, connection_points, **kwargs): + """Stablish WAN connectivity between the endpoints + + Arguments: + service_type (str): ``ELINE`` (L2), ``ELAN`` (L2), ``ETREE`` (L2), + ``L3``. + connection_points (list): each point corresponds to + an entry point from the DC to the transport network. One + connection point serves to identify the specific access and + some other service parameters, such as encapsulation type. + Represented by a dict as follows:: + + { + "service_endpoint_id": ..., (str[uuid]) + "service_endpoint_encapsulation_type": ..., + (enum: none, dot1q, ...) + "service_endpoint_encapsulation_info": { + ... (dict) + "vlan": ..., (int, present if encapsulation is dot1q) + "vni": ... (int, present if encapsulation is vxlan), + "peers": [(ipv4_1), (ipv4_2)] + (present if encapsulation is vxlan) + } + } + + The service endpoint ID should be previously informed to the WIM + engine in the RO when the WIM port mapping is registered. + + Keyword Arguments: + bandwidth (int): value in kilobytes + latency (int): value in milliseconds + + Other QoS might be passed as keyword arguments. + + Returns: + tuple: ``(service_id, conn_info)`` containing: + - *service_uuid* (str): UUID of the established connectivity + service + - *conn_info* (dict or None): Information to be stored at the + database (or ``None``). This information will be provided to + the :meth:`~.edit_connectivity_service` and :obj:`~.delete`. + **MUST** be JSON/YAML-serializable (plain data structures). + + Raises: + SdnConnectorException: In case of error. + """ + if service_type == "ELINE": + if len(connection_points) > 2: + raise SdnConnectorError( + "Connections between more than 2 endpoints are not supported" + ) + + if len(connection_points) < 2: + raise SdnConnectorError("Connections must be of at least 2 endpoints") + + """First step, create the vpn service""" + uuid_l2vpn = str(uuid.uuid4()) + vpn_service = {} + vpn_service["vpn-id"] = uuid_l2vpn + vpn_service["vpn-svc-type"] = "vpws" # Rename "vpn-scv-type" -> "vpn-svc-type" + vpn_service["svc-topo"] = "any-to-any" + vpn_service["customer-name"] = "osm" + vpn_service_list = [] + vpn_service_list.append(vpn_service) + vpn_service_l = {"ietf-l2vpn-svc:vpn-service": vpn_service_list} + response_service_creation = None + conn_info = [] + self.logger.info("Sending vpn-service :{}".format(vpn_service_l)) + + try: + endpoint_service_creation = ( + "{}/restconf/data/ietf-l2vpn-svc:l2vpn-svc/vpn-services".format( + self.wim["wim_url"] + ) + ) + response_service_creation = requests.post( + endpoint_service_creation, + headers=self.headers, + json=vpn_service_l, + auth=self.auth, + ) + except requests.exceptions.ConnectionError: + raise SdnConnectorError( + "Request to create service Timeout", http_code=408 + ) + + if response_service_creation.status_code == 409: + raise SdnConnectorError( + "Service already exists", + http_code=response_service_creation.status_code, + ) + elif response_service_creation.status_code != requests.codes.created: + raise SdnConnectorError( + "Request to create service not accepted", + http_code=response_service_creation.status_code, + ) + + """Second step, create the connections and vpn attachments""" + for connection_point in connection_points: + connection_point_wan_info = self.search_mapp(connection_point) + site_network_access = {} + connection = {} + + if connection_point["service_endpoint_encapsulation_type"] != "none": + if ( + connection_point["service_endpoint_encapsulation_type"] + == "dot1q" + ): + """The connection is a VLAN""" + connection["encapsulation-type"] = "dot1q-vlan-tagged" + tagged = {} + tagged_interf = {} + service_endpoint_encapsulation_info = connection_point[ + "service_endpoint_encapsulation_info" + ] + + if service_endpoint_encapsulation_info["vlan"] is None: + raise SdnConnectorError("VLAN must be provided") + + tagged_interf["cvlan-id"] = service_endpoint_encapsulation_info[ + "vlan" + ] + tagged["dot1q-vlan-tagged"] = tagged_interf + connection["tagged-interface"] = tagged + else: + raise NotImplementedError("Encapsulation type not implemented") + + site_network_access["connection"] = connection + self.logger.info("Sending connection:{}".format(connection)) + vpn_attach = {} + vpn_attach["vpn-id"] = uuid_l2vpn + vpn_attach["site-role"] = vpn_service["svc-topo"] + "-role" + site_network_access["vpn-attachment"] = vpn_attach + self.logger.info("Sending vpn-attachement :{}".format(vpn_attach)) + uuid_sna = str(uuid.uuid4()) + site_network_access["network-access-id"] = uuid_sna + site_network_access["bearer"] = connection_point_wan_info[ + "service_mapping_info" + ]["bearer"] + site_network_accesses = {} + site_network_access_list = [] + site_network_access_list.append(site_network_access) + site_network_accesses[ + "ietf-l2vpn-svc:site-network-access" + ] = site_network_access_list + conn_info_d = {} + conn_info_d["site"] = connection_point_wan_info["service_mapping_info"][ + "site-id" + ] + conn_info_d["site-network-access-id"] = site_network_access[ + "network-access-id" + ] + conn_info_d["mapping"] = None + conn_info.append(conn_info_d) + + try: + endpoint_site_network_access_creation = ( + "{}/restconf/data/ietf-l2vpn-svc:l2vpn-svc/" + "sites/site={}/site-network-accesses/".format( + self.wim["wim_url"], + connection_point_wan_info["service_mapping_info"][ + "site-id" + ], + ) + ) + response_endpoint_site_network_access_creation = requests.post( + endpoint_site_network_access_creation, + headers=self.headers, + json=site_network_accesses, + auth=self.auth, + ) + + if ( + response_endpoint_site_network_access_creation.status_code + == 409 + ): + self.delete_connectivity_service(vpn_service["vpn-id"]) + + raise SdnConnectorError( + "Site_Network_Access with ID '{}' already exists".format( + site_network_access["network-access-id"] + ), + http_code=response_endpoint_site_network_access_creation.status_code, + ) + elif ( + response_endpoint_site_network_access_creation.status_code + == 400 + ): + self.delete_connectivity_service(vpn_service["vpn-id"]) + + raise SdnConnectorError( + "Site {} does not exist".format( + connection_point_wan_info["service_mapping_info"][ + "site-id" + ] + ), + http_code=response_endpoint_site_network_access_creation.status_code, + ) + elif ( + response_endpoint_site_network_access_creation.status_code + != requests.codes.created + and response_endpoint_site_network_access_creation.status_code + != requests.codes.no_content + ): + self.delete_connectivity_service(vpn_service["vpn-id"]) + + raise SdnConnectorError( + "Request no accepted", + http_code=response_endpoint_site_network_access_creation.status_code, + ) + except requests.exceptions.ConnectionError: + self.delete_connectivity_service(vpn_service["vpn-id"]) + + raise SdnConnectorError("Request Timeout", http_code=408) + + return uuid_l2vpn, conn_info + else: + raise NotImplementedError + + def delete_connectivity_service(self, service_uuid, conn_info=None): + """Disconnect multi-site endpoints previously connected + + This method should receive as the first argument the UUID generated by + the ``create_connectivity_service`` + """ + try: + self.logger.info("Sending delete") + servicepoint = "{}/restconf/data/ietf-l2vpn-svc:l2vpn-svc/vpn-services/vpn-service={}/".format( + self.wim["wim_url"], service_uuid + ) + response = requests.delete(servicepoint, auth=self.auth) + + if response.status_code != requests.codes.no_content: + raise SdnConnectorError( + "Error in the request", http_code=response.status_code + ) + except requests.exceptions.ConnectionError: + raise SdnConnectorError("Request Timeout", http_code=408) + + def edit_connectivity_service( + self, service_uuid, conn_info=None, connection_points=None, **kwargs + ): + """Change an existing connectivity service, see + ``create_connectivity_service``""" + # sites = {"sites": {}} + # site_list = [] + vpn_service = {} + vpn_service["svc-topo"] = "any-to-any" + counter = 0 + + for connection_point in connection_points: + site_network_access = {} + connection_point_wan_info = self.search_mapp(connection_point) + params_site = {} + params_site["site-id"] = connection_point_wan_info["service_mapping_info"][ + "site-id" + ] + params_site["site-vpn-flavor"] = "site-vpn-flavor-single" + device_site = {} + device_site["device-id"] = connection_point_wan_info["device-id"] + params_site["devices"] = device_site + # network_access = {} + connection = {} + + if connection_point["service_endpoint_encapsulation_type"] != "none": + if connection_point["service_endpoint_encapsulation_type"] == "dot1q": + """The connection is a VLAN""" + connection["encapsulation-type"] = "dot1q-vlan-tagged" + tagged = {} + tagged_interf = {} + service_endpoint_encapsulation_info = connection_point[ + "service_endpoint_encapsulation_info" + ] + + if service_endpoint_encapsulation_info["vlan"] is None: + raise SdnConnectorError("VLAN must be provided") + + tagged_interf["cvlan-id"] = service_endpoint_encapsulation_info[ + "vlan" + ] + tagged["dot1q-vlan-tagged"] = tagged_interf + connection["tagged-interface"] = tagged + else: + raise NotImplementedError("Encapsulation type not implemented") + + site_network_access["connection"] = connection + vpn_attach = {} + vpn_attach["vpn-id"] = service_uuid + vpn_attach["site-role"] = vpn_service["svc-topo"] + "-role" + site_network_access["vpn-attachment"] = vpn_attach + uuid_sna = conn_info[counter]["site-network-access-id"] + site_network_access["network-access-id"] = uuid_sna + site_network_access["bearer"] = connection_point_wan_info[ + "service_mapping_info" + ]["bearer"] + site_network_accesses = {} + site_network_access_list = [] + site_network_access_list.append(site_network_access) + site_network_accesses[ + "ietf-l2vpn-svc:site-network-access" + ] = site_network_access_list + + try: + endpoint_site_network_access_edit = ( + "{}/restconf/data/ietf-l2vpn-svc:l2vpn-svc/" + "sites/site={}/site-network-accesses/".format( + self.wim["wim_url"], + connection_point_wan_info["service_mapping_info"]["site-id"], + ) + ) + response_endpoint_site_network_access_creation = requests.put( + endpoint_site_network_access_edit, + headers=self.headers, + json=site_network_accesses, + auth=self.auth, + ) + + if response_endpoint_site_network_access_creation.status_code == 400: + raise SdnConnectorError( + "Service does not exist", + http_code=response_endpoint_site_network_access_creation.status_code, + ) + elif ( + response_endpoint_site_network_access_creation.status_code != 201 + and response_endpoint_site_network_access_creation.status_code + != 204 + ): + raise SdnConnectorError( + "Request no accepted", + http_code=response_endpoint_site_network_access_creation.status_code, + ) + except requests.exceptions.ConnectionError: + raise SdnConnectorError("Request Timeout", http_code=408) + + counter += 1 + + return None + + def clear_all_connectivity_services(self): + """Delete all WAN Links corresponding to a WIM""" + try: + self.logger.info("Sending clear all connectivity services") + servicepoint = ( + "{}/restconf/data/ietf-l2vpn-svc:l2vpn-svc/vpn-services".format( + self.wim["wim_url"] + ) + ) + response = requests.delete(servicepoint, auth=self.auth) + + if response.status_code != requests.codes.no_content: + raise SdnConnectorError( + "Unable to clear all connectivity services", + http_code=response.status_code, + ) + except requests.exceptions.ConnectionError: + raise SdnConnectorError("Request Timeout", http_code=408) + + def get_all_active_connectivity_services(self): + """Provide information about all active connections provisioned by a + WIM + """ + try: + self.logger.info("Sending get all connectivity services") + servicepoint = ( + "{}/restconf/data/ietf-l2vpn-svc:l2vpn-svc/vpn-services".format( + self.wim["wim_url"] + ) + ) + response = requests.get(servicepoint, auth=self.auth) + + if response.status_code != requests.codes.ok: + raise SdnConnectorError( + "Unable to get all connectivity services", + http_code=response.status_code, + ) + + return response + except requests.exceptions.ConnectionError: + raise SdnConnectorError("Request Timeout", http_code=408) diff --git a/src/compute/tests/mock_osm/__init__.py b/src/compute/tests/mock_osm/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/compute/tests/mock_osm/acknowledgements.txt b/src/compute/tests/mock_osm/acknowledgements.txt new file mode 100644 index 0000000000000000000000000000000000000000..b7ce926dd006d9bc8afaffbed212d90fb05adbef --- /dev/null +++ b/src/compute/tests/mock_osm/acknowledgements.txt @@ -0,0 +1,3 @@ +MockOSM is based on source code taken from: +https://osm.etsi.org/gitlab/osm/ro/-/blob/master/RO-plugin/osm_ro_plugin/sdnconn.py +https://osm.etsi.org/gitlab/osm/ro/-/blob/master/RO-SDN-ietfl2vpn/osm_rosdn_ietfl2vpn/wimconn_ietfl2vpn.py diff --git a/src/compute/tests/mock_osm/sdnconn.py b/src/compute/tests/mock_osm/sdnconn.py new file mode 100644 index 0000000000000000000000000000000000000000..a1849c9ef3e1a1260ff42bbadabc99f91a6435d7 --- /dev/null +++ b/src/compute/tests/mock_osm/sdnconn.py @@ -0,0 +1,242 @@ +# -*- coding: utf-8 -*- +## +# Copyright 2018 University of Bristol - High Performance Networks Research +# Group +# All Rights Reserved. +# +# Contributors: Anderson Bravalheri, Dimitrios Gkounis, Abubakar Siddique +# Muqaddas, Navdeep Uniyal, Reza Nejabati and Dimitra Simeonidou +# +# 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. +# +# For those usages not covered by the Apache License, Version 2.0 please +# contact with: <highperformance-networks@bristol.ac.uk> +# +# Neither the name of the University of Bristol nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# This work has been performed in the context of DCMS UK 5G Testbeds +# & Trials Programme and in the framework of the Metro-Haul project - +# funded by the European Commission under Grant number 761727 through the +# Horizon 2020 and 5G-PPP programmes. +## +"""The SDN connector is responsible for establishing both wide area network connectivity (WIM) +and intranet SDN connectivity. + +It receives information from ports to be connected . +""" + +import logging +from http import HTTPStatus + + +class SdnConnectorError(Exception): + """Base Exception for all connector related errors + provide the parameter 'http_code' (int) with the error code: + Bad_Request = 400 + Unauthorized = 401 (e.g. credentials are not valid) + Not_Found = 404 (e.g. try to edit or delete a non existing connectivity service) + Forbidden = 403 + Method_Not_Allowed = 405 + Not_Acceptable = 406 + Request_Timeout = 408 (e.g timeout reaching server, or cannot reach the server) + Conflict = 409 + Service_Unavailable = 503 + Internal_Server_Error = 500 + """ + + def __init__(self, message, http_code=HTTPStatus.INTERNAL_SERVER_ERROR.value): + Exception.__init__(self, message) + self.http_code = http_code + + +class SdnConnectorBase(object): + """Abstract base class for all the SDN connectors + + Arguments: + wim (dict): WIM record, as stored in the database + wim_account (dict): WIM account record, as stored in the database + config + The arguments of the constructor are converted to object attributes. + An extra property, ``service_endpoint_mapping`` is created from ``config``. + """ + + def __init__(self, wim, wim_account, config=None, logger=None): + """ + :param wim: (dict). Contains among others 'wim_url' + :param wim_account: (dict). Contains among others 'uuid' (internal id), 'name', + 'sdn' (True if is intended for SDN-assist or False if intended for WIM), 'user', 'password'. + :param config: (dict or None): Particular information of plugin. These keys if present have a common meaning: + 'mapping_not_needed': (bool) False by default or if missing, indicates that mapping is not needed. + 'service_endpoint_mapping': (list) provides the internal endpoint mapping. The meaning is: + KEY meaning for WIM meaning for SDN assist + -------- -------- -------- + device_id pop_switch_dpid compute_id + device_interface_id pop_switch_port compute_pci_address + service_endpoint_id wan_service_endpoint_id SDN_service_endpoint_id + service_mapping_info wan_service_mapping_info SDN_service_mapping_info + contains extra information if needed. Text in Yaml format + switch_dpid wan_switch_dpid SDN_switch_dpid + switch_port wan_switch_port SDN_switch_port + datacenter_id vim_account vim_account + id: (internal, do not use) + wim_id: (internal, do not use) + :param logger (logging.Logger): optional logger object. If none is passed 'openmano.sdn.sdnconn' is used. + """ + self.logger = logger or logging.getLogger("ro.sdn") + self.wim = wim + self.wim_account = wim_account + self.config = config or {} + self.service_endpoint_mapping = self.config.get("service_endpoint_mapping", []) + + def check_credentials(self): + """Check if the connector itself can access the SDN/WIM with the provided url (wim.wim_url), + user (wim_account.user), and password (wim_account.password) + + Raises: + SdnConnectorError: Issues regarding authorization, access to + external URLs, etc are detected. + """ + raise NotImplementedError + + def get_connectivity_service_status(self, service_uuid, conn_info=None): + """Monitor the status of the connectivity service established + + Arguments: + service_uuid (str): UUID of the connectivity service + conn_info (dict or None): Information returned by the connector + during the service creation/edition and subsequently stored in + the database. + + Returns: + dict: JSON/YAML-serializable dict that contains a mandatory key + ``sdn_status`` associated with one of the following values:: + + {'sdn_status': 'ACTIVE'} + # The service is up and running. + + {'sdn_status': 'INACTIVE'} + # The service was created, but the connector + # cannot determine yet if connectivity exists + # (ideally, the caller needs to wait and check again). + + {'sdn_status': 'DOWN'} + # Connection was previously established, + # but an error/failure was detected. + + {'sdn_status': 'ERROR'} + # An error occurred when trying to create the service/ + # establish the connectivity. + + {'sdn_status': 'BUILD'} + # Still trying to create the service, the caller + # needs to wait and check again. + + Additionally ``error_msg``(**str**) and ``sdn_info``(**dict**) + keys can be used to provide additional status explanation or + new information available for the connectivity service. + """ + raise NotImplementedError + + def create_connectivity_service(self, service_type, connection_points, **kwargs): + """ + Establish SDN/WAN connectivity between the endpoints + :param service_type: (str): ``ELINE`` (L2), ``ELAN`` (L2), ``ETREE`` (L2), ``L3``. + :param connection_points: (list): each point corresponds to + an entry point to be connected. For WIM: from the DC to the transport network. + For SDN: Compute/PCI to the transport network. One + connection point serves to identify the specific access and + some other service parameters, such as encapsulation type. + Each item of the list is a dict with: + "service_endpoint_id": (str)(uuid) Same meaning that for 'service_endpoint_mapping' (see __init__) + In case the config attribute mapping_not_needed is True, this value is not relevant. In this case + it will contain the string "device_id:device_interface_id" + "service_endpoint_encapsulation_type": None, "dot1q", ... + "service_endpoint_encapsulation_info": (dict) with: + "vlan": ..., (int, present if encapsulation is dot1q) + "vni": ... (int, present if encapsulation is vxlan), + "peers": [(ipv4_1), (ipv4_2)] (present if encapsulation is vxlan) + "mac": ... + "device_id": ..., same meaning that for 'service_endpoint_mapping' (see __init__) + "device_interface_id": same meaning that for 'service_endpoint_mapping' (see __init__) + "switch_dpid": ..., present if mapping has been found for this device_id,device_interface_id + "swith_port": ... present if mapping has been found for this device_id,device_interface_id + "service_mapping_info": present if mapping has been found for this device_id,device_interface_id + :param kwargs: For future versions: + bandwidth (int): value in kilobytes + latency (int): value in milliseconds + Other QoS might be passed as keyword arguments. + :return: tuple: ``(service_id, conn_info)`` containing: + - *service_uuid* (str): UUID of the established connectivity service + - *conn_info* (dict or None): Information to be stored at the database (or ``None``). + This information will be provided to the :meth:`~.edit_connectivity_service` and :obj:`~.delete`. + **MUST** be JSON/YAML-serializable (plain data structures). + :raises: SdnConnectorException: In case of error. Nothing should be created in this case. + Provide the parameter http_code + """ + raise NotImplementedError + + def delete_connectivity_service(self, service_uuid, conn_info=None): + """ + Disconnect multi-site endpoints previously connected + + :param service_uuid: The one returned by create_connectivity_service + :param conn_info: The one returned by last call to 'create_connectivity_service' or 'edit_connectivity_service' + if they do not return None + :return: None + :raises: SdnConnectorException: In case of error. The parameter http_code must be filled + """ + raise NotImplementedError + + def edit_connectivity_service( + self, service_uuid, conn_info=None, connection_points=None, **kwargs + ): + """Change an existing connectivity service. + + This method's arguments and return value follow the same convention as + :meth:`~.create_connectivity_service`. + + :param service_uuid: UUID of the connectivity service. + :param conn_info: (dict or None): Information previously returned by last call to create_connectivity_service + or edit_connectivity_service + :param connection_points: (list): If provided, the old list of connection points will be replaced. + :param kwargs: Same meaning that create_connectivity_service + :return: dict or None: Information to be updated and stored at the database. + When ``None`` is returned, no information should be changed. + When an empty dict is returned, the database record will be deleted. + **MUST** be JSON/YAML-serializable (plain data structures). + Raises: + SdnConnectorException: In case of error. + """ + + def clear_all_connectivity_services(self): + """Delete all WAN Links in a WIM. + + This method is intended for debugging only, and should delete all the + connections controlled by the WIM/SDN, not only the connections that + a specific RO is aware of. + + Raises: + SdnConnectorException: In case of error. + """ + raise NotImplementedError + + def get_all_active_connectivity_services(self): + """Provide information about all active connections provisioned by a + WIM. + + Raises: + SdnConnectorException: In case of error. + """ + raise NotImplementedError diff --git a/src/compute/tests/test_unitary.py b/src/compute/tests/test_unitary.py index 689e6038251d0a371833b34cd9e5158af2fcc0e6..001999f1b9607f03cd393f5582cc08a504c0e9d2 100644 --- a/src/compute/tests/test_unitary.py +++ b/src/compute/tests/test_unitary.py @@ -1,48 +1,54 @@ -import logging, os, pytest, requests, time -from google.protobuf.json_format import MessageToDict -from common.type_checkers.Assertions import validate_service_id -from compute.client.ComputeClient import ComputeClient -from compute.proto.context_pb2 import Service -from compute.service.ComputeService import ComputeService -from compute.Config import ( - GRPC_SERVICE_PORT, GRPC_MAX_WORKERS, GRPC_GRACE_PERIOD, RESTAPI_SERVICE_PORT, RESTAPI_BASE_URL) -from compute.service.rest_server.Server import Server -from compute.service.rest_server.resources.Compute import Compute -from service.service.ServiceService import ServiceService -from service.Config import ( - GRPC_SERVICE_PORT as SERVICE_GRPC_SERVICE_PORT, GRPC_MAX_WORKERS as SERVICE_GRPC_MAX_WORKERS, - GRPC_GRACE_PERIOD as SERVICE_GRPC_GRACE_PERIOD) - -compute_grpc_port = 10000 + GRPC_SERVICE_PORT # avoid privileged ports -compute_restapi_port = 10000 + RESTAPI_SERVICE_PORT # avoid privileged ports -service_grpc_port = 10000 + SERVICE_GRPC_SERVICE_PORT # avoid privileged ports - -os.environ['SERVICESERVICE_SERVICE_HOST'] = '127.0.0.1' -os.environ['SERVICESERVICE_SERVICE_PORT_GRPC'] = str(service_grpc_port) +import logging, os, pytest, time +from compute.Config import RESTAPI_SERVICE_PORT, RESTAPI_BASE_URL +from compute.service.rest_server.RestServer import RestServer +from context.proto.context_pb2_grpc import add_ContextServiceServicer_to_server +from service.proto.service_pb2_grpc import add_ServiceServiceServicer_to_server +from .mock_osm.MockOSM import MockOSM +from .MockService import MockService +from .MockServicerImpl_Context import MockServicerImpl_Context +from .MockServicerImpl_Service import MockServicerImpl_Service LOGGER = logging.getLogger(__name__) LOGGER.setLevel(logging.DEBUG) -@pytest.fixture(scope='session') -def service_service(): - _service = ServiceService( - port=service_grpc_port, max_workers=SERVICE_GRPC_MAX_WORKERS, grace_period=SERVICE_GRPC_GRACE_PERIOD) - _service.start() - yield _service - _service.stop() +LOCALHOST = '127.0.0.1' +MOCKSERVER_GRPC_PORT = 10000 +COMPUTE_RESTAPI_PORT = 10000 + RESTAPI_SERVICE_PORT # avoid privileged ports + +class MockService_ContextService(MockService): + # Mock Server implementing Context and Service to simplify unitary tests of Compute + + def __init__(self, cls_name='MockService_Service'): + super().__init__(LOCALHOST, MOCKSERVER_GRPC_PORT, cls_name=cls_name) + + # pylint: disable=attribute-defined-outside-init + def install_servicers(self): + self.context_servicer = MockServicerImpl_Context() + add_ContextServiceServicer_to_server(self.context_servicer, self.server) + self.service_servicer = MockServicerImpl_Service() + add_ServiceServiceServicer_to_server(self.service_servicer, self.server) + +os.environ['CONTEXTSERVICE_SERVICE_HOST'] = LOCALHOST +os.environ['CONTEXTSERVICE_SERVICE_PORT_GRPC'] = str(MOCKSERVER_GRPC_PORT) +os.environ['SERVICESERVICE_SERVICE_HOST'] = LOCALHOST +os.environ['SERVICESERVICE_SERVICE_PORT_GRPC'] = str(MOCKSERVER_GRPC_PORT) + +# NBI Plugin IETF L2VPN requires environment variables CONTEXTSERVICE_SERVICE_HOST, CONTEXTSERVICE_SERVICE_PORT_GRPC, +# SERVICESERVICE_SERVICE_HOST, and SERVICESERVICE_SERVICE_PORT_GRPC to work properly. +# pylint: disable=wrong-import-position,ungrouped-imports +from compute.service.rest_server.nbi_plugins.ietf_l2vpn import register_ietf_l2vpn @pytest.fixture(scope='session') -def compute_service(service_service : ServiceService): - _service = ComputeService(port=compute_grpc_port, max_workers=GRPC_MAX_WORKERS, grace_period=GRPC_GRACE_PERIOD) +def mockservice(): + _service = MockService_ContextService() _service.start() yield _service _service.stop() @pytest.fixture(scope='session') -def compute_service_rest(): - _rest_server = Server(port=compute_restapi_port, base_url=RESTAPI_BASE_URL) - _rest_server.add_resource( - Compute, '/restconf/config/compute', endpoint='api.compute') +def compute_service_rest(mockservice): # pylint: disable=redefined-outer-name + _rest_server = RestServer(port=COMPUTE_RESTAPI_PORT, base_url=RESTAPI_BASE_URL) + register_ietf_l2vpn(_rest_server) _rest_server.start() time.sleep(1) # bring time for the server to start yield _rest_server @@ -50,27 +56,18 @@ def compute_service_rest(): _rest_server.join() @pytest.fixture(scope='session') -def compute_client(compute_service): - _client = ComputeClient(address='127.0.0.1', port=compute_grpc_port) - yield _client - _client.close() +def osm_wim(compute_service_rest): # pylint: disable=redefined-outer-name + wim_url = 'http://{:s}:{:d}'.format(LOCALHOST, COMPUTE_RESTAPI_PORT) + return MockOSM(wim_url) + +def test_compute_create_connectivity_service_rest_api(osm_wim : MockOSM): # pylint: disable=redefined-outer-name + osm_wim.create_connectivity_service() + +def test_compute_get_connectivity_service_status_rest_api(osm_wim : MockOSM): # pylint: disable=redefined-outer-name + osm_wim.get_connectivity_service_status() -def test_dummy_create_connectivity_service(compute_client : ComputeClient): - # dummy test: should fail with assertion error - with pytest.raises(AssertionError): - validate_service_id(MessageToDict( - compute_client.CreateConnectivityService(Service()), - including_default_value_fields=True, preserving_proto_field_name=True, - use_integers_for_enums=False)) +def test_compute_edit_connectivity_service_rest_api(osm_wim : MockOSM): # pylint: disable=redefined-outer-name + osm_wim.edit_connectivity_service() -def test_dummy_create_connectivity_service_rest_api(compute_service_rest : Server): - # should work - request_url = 'http://127.0.0.1:{:s}{:s}/restconf/config/compute' - request_url = request_url.format(str(compute_restapi_port), RESTAPI_BASE_URL) - reply = requests.post(request_url, json={ - # here add context of POST request body as JSON - }) - json_reply = reply.json() - LOGGER.info('json_reply = {:s}'.format(str(json_reply))) - assert 'succeeded' in json_reply - assert json_reply['succeeded'] +def test_compute_delete_connectivity_service_rest_api(osm_wim : MockOSM): # pylint: disable=redefined-outer-name + osm_wim.delete_connectivity_service() diff --git a/src/context/.gitlab-ci.yml b/src/context/.gitlab-ci.yml index 23949213fed4941813b38ab39255648b8ad9364b..bae78b86d55c0437a468bff03c30afa28598d5c7 100644 --- a/src/context/.gitlab-ci.yml +++ b/src/context/.gitlab-ci.yml @@ -10,8 +10,8 @@ build context: - docker build -t "$IMAGE_NAME:$IMAGE_TAG" -f ./src/$IMAGE_NAME/Dockerfile ./src/ - docker tag "$IMAGE_NAME:$IMAGE_TAG" "$CI_REGISTRY_IMAGE/$IMAGE_NAME:$IMAGE_TAG" - docker push "$CI_REGISTRY_IMAGE/$IMAGE_NAME:$IMAGE_TAG" -# after_script: -# - docker rmi $(docker images --quiet --filter=dangling=true) + after_script: + - docker images --filter="dangling=true" --quiet | xargs -r docker rmi rules: - if: '$CI_PIPELINE_SOURCE == "merge_request_event" && ($CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "develop" || $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == $CI_DEFAULT_BRANCH)' - if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "develop"' @@ -38,10 +38,11 @@ unit test context: script: - docker pull "$CI_REGISTRY_IMAGE/$IMAGE_NAME:$IMAGE_TAG" - docker pull "redis:6.2" - - docker run --name redis -d --network=teraflowbridge --rm redis:6.2 - - docker run --name $IMAGE_NAME -d -p 1010:1010 --env "DB_BACKEND=redis" --env "REDIS_SERVICE_HOST=redis" --env "REDIS_SERVICE_PORT=6379" --env "REDIS_DATABASE_ID=0" --network=teraflowbridge --rm $CI_REGISTRY_IMAGE/$IMAGE_NAME:$IMAGE_TAG + - docker run --name redis -d --network=teraflowbridge redis:6.2 + - docker run --name $IMAGE_NAME -d -p 1010:1010 --env "DB_BACKEND=redis" --env "REDIS_SERVICE_HOST=redis" --env "REDIS_SERVICE_PORT=6379" --env "REDIS_DATABASE_ID=0" --network=teraflowbridge $CI_REGISTRY_IMAGE/$IMAGE_NAME:$IMAGE_TAG - sleep 10 - docker ps -a + - docker logs $IMAGE_NAME - docker exec -i $IMAGE_NAME bash -c "pytest --log-level=DEBUG --verbose $IMAGE_NAME/tests/test_unitary.py" after_script: - docker rm -f $IMAGE_NAME diff --git a/src/context/tests/test_unitary.py b/src/context/tests/test_unitary.py index 750fa2a311bd35d0f47e04da3e424ca9d10345f7..5836511e1fe6057a55ee714d1b0e9a905f638660 100644 --- a/src/context/tests/test_unitary.py +++ b/src/context/tests/test_unitary.py @@ -318,16 +318,22 @@ def test_grpc_topology( assert response.context_uuid.uuid == DEFAULT_CONTEXT_UUID # ----- Check create event ----------------------------------------------------------------------------------------- - event = events_collector.get_event(block=True) - assert isinstance(event, TopologyEvent) - assert event.event.event_type == EventTypeEnum.EVENTTYPE_CREATE - assert event.topology_id.context_id.context_uuid.uuid == DEFAULT_CONTEXT_UUID - assert event.topology_id.topology_uuid.uuid == DEFAULT_TOPOLOGY_UUID - - event = events_collector.get_event(block=True) - assert isinstance(event, ContextEvent) - assert event.event.event_type == EventTypeEnum.EVENTTYPE_UPDATE - assert event.context_id.context_uuid.uuid == DEFAULT_CONTEXT_UUID + context_event = None + topology_event = None + for _ in range(2): + event = events_collector.get_event(block=True) + assert isinstance(event, (ContextEvent, TopologyEvent)) + if isinstance(event, ContextEvent): context_event = event + if isinstance(event, TopologyEvent): topology_event = event + + assert context_event is not None + assert context_event.event.event_type == EventTypeEnum.EVENTTYPE_UPDATE + assert context_event.context_id.context_uuid.uuid == DEFAULT_CONTEXT_UUID + + assert topology_event is not None + assert topology_event.event.event_type == EventTypeEnum.EVENTTYPE_CREATE + assert topology_event.topology_id.context_id.context_uuid.uuid == DEFAULT_CONTEXT_UUID + assert topology_event.topology_id.topology_uuid.uuid == DEFAULT_TOPOLOGY_UUID # ----- Update the object ------------------------------------------------------------------------------------------ response = context_client_grpc.SetTopology(Topology(**TOPOLOGY)) diff --git a/src/device/.gitlab-ci.yml b/src/device/.gitlab-ci.yml index 8656e01fede917d6a6ef13d0c0cb5fac618c444c..2c93618afd4d6617370f0567bb33b4b4d382a1cf 100644 --- a/src/device/.gitlab-ci.yml +++ b/src/device/.gitlab-ci.yml @@ -10,8 +10,8 @@ build device: - docker build -t "$IMAGE_NAME:$IMAGE_TAG" -f ./src/$IMAGE_NAME/Dockerfile ./src/ - docker tag "$IMAGE_NAME:$IMAGE_TAG" "$CI_REGISTRY_IMAGE/$IMAGE_NAME:$IMAGE_TAG" - docker push "$CI_REGISTRY_IMAGE/$IMAGE_NAME:$IMAGE_TAG" -# after_script: -# - docker rmi $(docker images --quiet --filter=dangling=true) + after_script: + - docker images --filter="dangling=true" --quiet | xargs -r docker rmi rules: - if: '$CI_PIPELINE_SOURCE == "merge_request_event" && ($CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "develop" || $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == $CI_DEFAULT_BRANCH)' - if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "develop"' @@ -36,9 +36,10 @@ unit test device: - if docker container ls | grep $IMAGE_NAME; then docker rm -f $IMAGE_NAME; else echo "$IMAGE_NAME image is not in the system"; fi script: - docker pull "$CI_REGISTRY_IMAGE/$IMAGE_NAME:$IMAGE_TAG" - - docker run --name $IMAGE_NAME -d -p 2020:2020 --network=teraflowbridge --rm $CI_REGISTRY_IMAGE/$IMAGE_NAME:$IMAGE_TAG + - docker run --name $IMAGE_NAME -d -p 2020:2020 --network=teraflowbridge $CI_REGISTRY_IMAGE/$IMAGE_NAME:$IMAGE_TAG - sleep 5 - docker ps -a + - docker logs $IMAGE_NAME - docker exec -i $IMAGE_NAME bash -c "pytest --log-level=DEBUG --verbose $IMAGE_NAME/tests/test_unitary.py" after_script: - docker rm -f $IMAGE_NAME diff --git a/src/service/.gitlab-ci.yml b/src/service/.gitlab-ci.yml index d47177171a768a776368561c20aae1658bdc614d..3c943d5438f64dd1e6f6b12749b839539a22ca46 100644 --- a/src/service/.gitlab-ci.yml +++ b/src/service/.gitlab-ci.yml @@ -10,8 +10,8 @@ build service: - docker build -t "$IMAGE_NAME:$IMAGE_TAG" -f ./src/$IMAGE_NAME/Dockerfile ./src/ - docker tag "$IMAGE_NAME:$IMAGE_TAG" "$CI_REGISTRY_IMAGE/$IMAGE_NAME:$IMAGE_TAG" - docker push "$CI_REGISTRY_IMAGE/$IMAGE_NAME:$IMAGE_TAG" -# after_script: -# - docker rmi $(docker images --quiet --filter=dangling=true) + after_script: + - docker images --filter="dangling=true" --quiet | xargs -r docker rmi rules: - if: '$CI_PIPELINE_SOURCE == "merge_request_event" && ($CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "develop" || $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == $CI_DEFAULT_BRANCH)' - if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "develop"'