Skip to content
Snippets Groups Projects
Commit 84f998e9 authored by Lluis Gifre Renom's avatar Lluis Gifre Renom
Browse files

Merge branch 'feat/compute-component' into 'develop'

First functional version of Compute component

See merge request teraflow-h2020/controller!53
parents cb66db4b d8b14fda
No related branches found
No related tags found
1 merge request!54Release 2.0.0
Showing
with 439 additions and 22 deletions
......@@ -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'
......
......@@ -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
......
......@@ -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
---
......
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({}).'
......
......@@ -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
......
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
......@@ -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"]
Flask
Flask-HTTPAuth
Flask-RESTful
grpcio-health-checking
grpcio
jsonschema
prometheus-client
pytest
pytest-benchmark
requests
......@@ -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')
......@@ -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
......
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):
......
DEFAULT_MTU = 1512
DEFAULT_ADDRESS_FAMILIES = ['IPV4']
DEFAULT_SUB_INTERFACE_INDEX = 0
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
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
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)
# 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/')
# 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}'
# 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},
},
},
},
},
},
},
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment