Skip to content
Snippets Groups Projects
Commit 21a72d98 authored by Manuel Angel Jimenez Quesada's avatar Manuel Angel Jimenez Quesada
Browse files

Add HTTP Server and gRPC Server

Add manifest.yaml
Add DockerFile

Solve some typo on .proto file
parent 11b4748c
No related branches found
No related tags found
2 merge requests!359Release TeraFlowSDN 5.0,!329Resolve "Add ZTP server that provide a zero touch provisioning to whiteboxes"
Showing
with 123 additions and 1102 deletions
......@@ -12,24 +12,67 @@
# See the License for the specific language governing permissions and
# limitations under the License.
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)
apiVersion: apps/v1
kind: Deployment
metadata:
name: ztp_serverservice
spec:
selector:
matchLabels:
app: ztp_serverservice
#replicas: 1
template:
metadata:
labels:
app: ztp_serverservice
spec:
terminationGracePeriodSeconds: 5
containers:
- name: server
image: labs.etsi.org:5050/tfs/controller/ztp_server:latest
imagePullPolicy: Always
ports:
- containerPort: 8005
- containerPort: 5051
- containerPort: 9192
env:
- name: LOG_LEVEL
value: "INFO"
readinessProbe:
exec:
command: ["/bin/grpc_health_probe", "-addr=:5051"]
livenessProbe:
exec:
command: ["/bin/grpc_health_probe", "-addr=:5051"]
resources:
requests:
cpu: 250m
memory: 128Mi
limits:
cpu: 1000m
memory: 1024Mi
---
apiVersion: v1
kind: Service
metadata:
name: ztp_serverservice
labels:
app: ztp_serverservice
spec:
type: ClusterIP
selector:
app: ztp_serverservice
ports:
- name: http
protocol: TCP
port: 8005
targetPort: 8005
- name: grpc
protocol: TCP
port: 5051
targetPort: 5051
- name: metrics
protocol: TCP
port: 9192
targetPort:
---
......@@ -25,11 +25,11 @@ service ZtpServerService {
// Define the request message for both methods
message ProvisioningScriptName {
string input = 1;
string scriptname = 1;
}
message ZtpFileName {
string input = 1;
string filename = 1;
}
message ProvisioningScript {
......
......@@ -122,8 +122,9 @@ DEFAULT_SERVICE_GRPC_PORTS = {
# Default HTTP/REST-API service ports
DEFAULT_SERVICE_HTTP_PORTS = {
ServiceNameEnum.NBI .value : 8080,
ServiceNameEnum.WEBUI.value : 8004,
ServiceNameEnum.NBI .value : 8080,
ServiceNameEnum.WEBUI.value : 8004,
ServiceNameEnum.ZTP_SERVER.value : 8005,
}
# Default HTTP/REST-API service base URLs
......
......@@ -19,21 +19,6 @@ RUN apt-get --yes --quiet --quiet update && \
apt-get --yes --quiet --quiet install wget g++ git build-essential cmake libpcre2-dev python3-dev python3-cffi && \
rm -rf /var/lib/apt/lists/*
# Download, build and install libyang. Note that APT package is outdated
# - Ref: https://github.com/CESNET/libyang
# - Ref: https://github.com/CESNET/libyang-python/
RUN mkdir -p /var/libyang
RUN git clone https://github.com/CESNET/libyang.git /var/libyang
WORKDIR /var/libyang
RUN git fetch
RUN git checkout v2.1.148
RUN mkdir -p /var/libyang/build
WORKDIR /var/libyang/build
RUN cmake -D CMAKE_BUILD_TYPE:String="Release" ..
RUN make
RUN make install
RUN ldconfig
# Set Python to show logs as they occur
ENV PYTHONUNBUFFERED=0
......@@ -69,29 +54,19 @@ RUN rm *.proto
RUN find . -type f -exec sed -i -E 's/(import\ .*)_pb2/from . \1_pb2/g' {} \;
# Create component sub-folders, get specific Python packages
RUN mkdir -p /var/teraflow/nbi
WORKDIR /var/teraflow/nbi
COPY src/nbi/requirements.in requirements.in
RUN mkdir -p /var/teraflow/ztp_server
WORKDIR /var/teraflow/ztp_server
COPY src/ztp_server/requirements.in requirements.in
RUN pip-compile --quiet --output-file=requirements.txt requirements.in
RUN python3 -m pip install -r requirements.txt
# Add component files into working directory
WORKDIR /var/teraflow
COPY src/nbi/. nbi/
COPY src/context/__init__.py context/__init__.py
COPY src/context/client/. context/client/
COPY src/device/__init__.py device/__init__.py
COPY src/device/client/. device/client/
COPY src/service/__init__.py service/__init__.py
COPY src/service/client/. service/client/
COPY src/slice/__init__.py slice/__init__.py
COPY src/slice/client/. slice/client/
COPY src/qkd_app/__init__.py qkd_app/__init__.py
COPY src/qkd_app/client/. qkd_app/client/
COPY src/vnt_manager/__init__.py vnt_manager/__init__.py
COPY src/vnt_manager/client/. vnt_manager/client/
RUN mkdir -p /var/teraflow/tests/tools
COPY src/tests/tools/mock_osm/. tests/tools/mock_osm/
COPY src/ztp_server/. ztp_server/
#ToDo Implement Test
#RUN mkdir -p /var/teraflow/tests/tools
#COPY src/tests/tools/mock_osm/. tests/tools/mock_osm/
# Start the service
ENTRYPOINT ["python", "-m", "nbi.service"]
ENTRYPOINT ["python", "-m", "ztp_server.service"]
# NBI Component
The NBI component uses libyang to validate and process messages. Follow instructions below to install it.
## Install libyang
- Ref: https://github.com/CESNET/libyang
- Ref: https://github.com/CESNET/libyang-python/
__NOTE__: APT package is extremely outdated and does not work for our purposes.
### Build Requisites
```bash
sudo apt-get install build-essential cmake libpcre2-dev
sudo apt-get install python3-dev gcc python3-cffi
```
### Build from source
```bash
mkdir ~/tfs-ctrl/libyang
git clone https://github.com/CESNET/libyang.git ~/tfs-ctrl/libyang
cd ~/tfs-ctrl/libyang
git fetch
git checkout v2.1.148
mkdir ~/tfs-ctrl/libyang/build
cd ~/tfs-ctrl/libyang/build
cmake -D CMAKE_BUILD_TYPE:String="Release" ..
make
sudo make install
sudo ldconfig
```
### Install Python bindings
```bash
pip install libyang==2.8.0
```
......@@ -15,9 +15,8 @@
import grpc, logging
from common.Constants import ServiceNameEnum
from common.Settings import get_service_host, get_service_port_grpc
from common.proto.ztp_server_pb2_grpc import ztpServerServiceStub
from common.proto.context_pb2 import (
ZtpFileName, ZtpFile, ProvisioningScriptName, ProvisioningScript)
from common.proto.ztp_server_pb2_grpc import ZtpServerServiceStub
from common.proto.ztp_server_pb2 import ProvisioningScriptName, ProvisioningScript, ZtpFileName, ZtpFile
from common.tools.client.RetryDecorator import retry, delay_exponential
from common.tools.grpc.Tools import grpc_message_to_json_string
......@@ -39,7 +38,7 @@ class ZtpClient:
def connect(self):
self.channel = grpc.insecure_channel(self.endpoint)
self.stub = ztpServerServiceStub(self.channel)
self.stub = ZtpServerServiceStub(self.channel)
def close(self):
if self.channel is not None: self.channel.close()
......
// Copyright 2022-2023 ETSI TeraFlowSDN - TFS OSG (https://tfs.etsi.org/)
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
syntax = "proto3";
package nos_client;
message NOS_SW {
string ztp_device_sw_url = 1;
bytes ztp_device_sw_file = 2;
}
// For ONIE Requests
message NOS_SW_REQ {
string serial_number = 1;
string eth_addr = 2;
string vendor_id = 3;
string machine = 4;
string machine_rev = 5;
string arch = 6;
string security_key = 7;
string operation = 8;
}
message Config_Script {
string config_script_url = 1;
bytes config_script_file = 2;
}
message Config_Script_REQ {
string agent = 1;
string machine = 2;
string serial_number = 3;
string eth_addr = 4;
string version = 5;
}
message Status_Notification {
string status = 1;
string serial_number = 2;
string eth_addr = 3;
}
message Empty{}
service nos_client {
rpc GetNOSFile (NOS_SW_REQ) returns (NOS_SW) {}
rpc GetConfigScriptFile (Config_Script_REQ) returns (Config_Script) {}
rpc NotifyStatus (Status_Notification) returns (Empty) {}
}
......@@ -18,10 +18,7 @@ Flask==2.1.3
Flask-HTTPAuth==4.5.0
Flask-RESTful==0.3.9
jsonschema==4.4.0
libyang==2.8.0
netaddr==0.9.0
pyang==2.6.0
git+https://github.com/robshakir/pyangbind.git
pydantic==2.6.3
requests==2.27.1
werkzeug==2.3.7
......
......@@ -14,7 +14,7 @@
from common.Constants import ServiceNameEnum
from common.Settings import get_service_port_grpc
from common.proto.ztp_server_pb2_grpc import add_Ztp_ServerServiceServicer_to_server
from common.proto.ztp_server_pb2_grpc import add_ZtpServerServiceServicer_to_server
from common.tools.service.GenericGrpcService import GenericGrpcService
from ztp_server.service.ZtpServerServiceServicerImpl import ZtpServerServiceServicerImpl
......@@ -25,4 +25,4 @@ class ZtpServerService(GenericGrpcService):
self.ztp_servicer = ZtpServerServiceServicerImpl()
def install_servicers(self):
add_Ztp_ServerServiceServicer_to_server(self.ztp_servicer, self.server)
add_ZtpServerServiceServicer_to_server(self.ztp_servicer, self.server)
......@@ -12,17 +12,17 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import grpc, logging, json
import grpc, logging, json, os
from common.method_wrappers.Decorator import MetricsPool, safe_and_metered_rpc_method
from common.proto.context_pb2 import (
ZtpFileName, ZtpFile, ProvisioningScriptName, ProvisioningScript)
from common.proto.ztp_server_pb2_grpc import ztpServerServiceServicer
from common.proto.ztp_server_pb2 import ProvisioningScriptName, ProvisioningScript, ZtpFileName, ZtpFile
from common.proto.ztp_server_pb2_grpc import ZtpServerServiceServicer
LOGGER = logging.getLogger(__name__)
METRICS_POOL = MetricsPool('ZTP_SERVER', 'RPC')
class ZtpServerServiceServicerImpl(ztpServerServiceServicer):
class ZtpServerServiceServicerImpl(ZtpServerServiceServicer):
def __init__(self):
LOGGER.info('Creating Servicer...')
LOGGER.info('Servicer Created')
......@@ -30,24 +30,24 @@ class ZtpServerServiceServicerImpl(ztpServerServiceServicer):
@safe_and_metered_rpc_method(METRICS_POOL, LOGGER)
def GetZtpProvisioning(self, request : ProvisioningScriptName, context : grpc.ServicerContext) -> ProvisioningScript:
try:
filePath = '../data/' + ProvisioningScriptName
with open(filePath, 'r') as provisioning_file:
provisioningPath = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', '..', 'data', request.scriptname)
with open(provisioningPath, 'r') as provisioning_file:
provisioning_content = provisioning_file.read()
return ztpServerServiceServicer.ProvisioningScript(script=provisioning_content)
return ProvisioningScript(script=provisioning_content)
except FileNotFoundError:
context.set_code(grpc.StatusCode.NOT_FOUND)
context.set_details('File not found')
return ztpServerServiceServicer.ProvisioningScript()
return ProvisioningScript()
@safe_and_metered_rpc_method(METRICS_POOL, LOGGER)
def GetZtpProvisioning(self, request : ZtpFileName, context : grpc.ServicerContext) -> ZtpFile:
try:
filePath = '../data/' + ZtpFileName
with open(filePath, 'r') as json_file:
ztpPath = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', '..', 'data', request.filename)
with open(ztpPath, 'r') as json_file:
json_content = json_file.read()
return ztpServerServiceServicer.ZtpFile(json=json_content)
return ZtpFile(json=json_content)
except FileNotFoundError:
context.set_code(grpc.StatusCode.NOT_FOUND)
context.set_details('File not found')
return ztpServerServiceServicer.ZtpFile(json=json_content)
\ No newline at end of file
return ZtpFile()
\ No newline at end of file
# Copyright 2022-2024 ETSI SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/)
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import logging
from websockets.sync.server import serve
from common.proto.vnt_manager_pb2 import VNTSubscriptionRequest
from common.Settings import get_setting
from context.client.ContextClient import ContextClient
from common.Constants import DEFAULT_CONTEXT_NAME, DEFAULT_TOPOLOGY_NAME
from common.tools.object_factory.Topology import json_topology_id
from common.tools.object_factory.Context import json_context_id
from common.proto.context_pb2 import ContextId, TopologyId
import json
import os
from vnt_manager.client.VNTManagerClient import VNTManagerClient
JSON_ADMIN_CONTEXT_ID = json_context_id(DEFAULT_CONTEXT_NAME)
ADMIN_CONTEXT_ID = ContextId(**JSON_ADMIN_CONTEXT_ID)
ADMIN_TOPOLOGY_ID = TopologyId(**json_topology_id(DEFAULT_TOPOLOGY_NAME, context_id=JSON_ADMIN_CONTEXT_ID))
vnt_manager_client: VNTManagerClient = VNTManagerClient()
context_client: ContextClient = ContextClient()
ALL_HOSTS = "0.0.0.0"
WS_E2E_PORT = int(get_setting('WS_E2E_PORT', default='8762'))
LOGGER = logging.getLogger(__name__)
def register_context_subscription():
with serve(subcript_to_vnt_manager, ALL_HOSTS, WS_E2E_PORT, logger=LOGGER) as server:
LOGGER.info("Running subscription server...: {}:{}".format(ALL_HOSTS, str(WS_E2E_PORT)))
server.serve_forever()
LOGGER.info("Exiting subscription server...")
def subcript_to_vnt_manager(websocket):
for message in websocket:
LOGGER.debug("Message received: {}".format(message))
message_json = json.loads(message)
request = VNTSubscriptionRequest()
request.host = message_json['host']
request.port = message_json['port']
LOGGER.debug("Received gRPC from ws: {}".format(request))
try:
vntm_reply = vnt_manager_client.VNTSubscript(request)
LOGGER.debug("Received gRPC from vntm: {}".format(vntm_reply))
except Exception as e:
LOGGER.error('Could not subscript to VTNManager: {}'.format(e))
websocket.send(vntm_reply.subscription)
......@@ -16,32 +16,22 @@ import json
import logging
from flask.json import jsonify
from flask_restful import Resource, request
from werkzeug.exceptions import BadRequest
from common.proto.context_pb2 import Empty, LinkTypeEnum
from common.tools.grpc.Tools import grpc_message_to_json
from context.client.ContextClient import ContextClient
from device.client.DeviceClient import DeviceClient
from service.client.ServiceClient import ServiceClient
from slice.client.SliceClient import SliceClient
from vnt_manager.client.VNTManagerClient import VNTManagerClient
from ztp_server.service.rest_server.ztpServer_plugins.tools.Authentication import HTTP_AUTH
from .Tools import (
format_grpc_to_json, returnConfigFile
returnConfigFile
)
LOGGER = logging.getLogger(__name__)
class _Resource(Resource):
def __init__(self) -> None:
super().__init__()
self.context_client = ContextClient()
self.device_client = DeviceClient()
self.service_client = ServiceClient()
self.vntmanager_client = VNTManagerClient()
self.slice_client = SliceClient()
class config(_Resource):
@HTTP_AUTH.login_required
def get(self, config_db : str):
return returnConfigFile(config_db) #TODO define how to return configFile.json
#if returnConfigFile(config_db)
return
......@@ -12,30 +12,14 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from typing import Dict
from flask.json import jsonify
from common.proto.context_pb2 import (
ConnectionId, returnConfigFile
)
from common.proto.policy_pb2 import PolicyRule, PolicyRuleId
from common.tools.grpc.Tools import grpc_message_to_json
from common.tools.object_factory.Connection import json_connection_id
from common.tools.object_factory.Context import json_context_id
from common.tools.object_factory.Device import json_device_id
from common.tools.object_factory.Link import json_link_id
from common.tools.object_factory.PolicyRule import json_policyrule_id
from common.tools.object_factory.Service import json_service_id
from common.tools.object_factory.Slice import json_slice_id
from common.tools.object_factory.Topology import json_topology_id
def format_grpc_to_json(grpc_reply):
return jsonify(grpc_message_to_json(grpc_reply))
import os
def returnConfigFile(config_db):
path = config_db
with open(path, 'r', encoding='utf-8') as configFile:
content = configFile.read()
try:
configFilePath = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..','..','..','..','..', 'data', config_db)
with open(configFilePath, 'r', encoding='utf-8') as configFile:
content = configFile.read()
except FileNotFoundError:
return "File not Found"
return content
\ No newline at end of file
# Copyright 2022-2024 ETSI SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/)
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
USERNAME = 'admin'
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': 'R1-EMU:13/1/2'},
'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': 'R2-EMU:13/1/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': 'R3-EMU:13/1/2'},
'site-id': '3',
},
#'switch_dpid' : ??, # wan_switch_dpid
#'switch_port' : ??, # wan_switch_port
#'datacenter_id' : ??, # vim_account
},
{
'device-id' : 'dev-4', # pop_switch_dpid
#'device_interface_id' : ??, # pop_switch_port
'service_endpoint_id' : 'ep-4', # wan_service_endpoint_id
'service_mapping_info': { # wan_service_mapping_info, other extra info
'bearer': {'bearer-reference': 'R4-EMU:13/1/2'},
'site-id': '4',
},
#'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}},
]
\ No newline at end of file
# Copyright 2022-2024 ETSI SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/)
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import os
from typing import Union
from common.Constants import ServiceNameEnum
from common.Settings import ENVVAR_SUFIX_SERVICE_HOST, ENVVAR_SUFIX_SERVICE_PORT_GRPC, get_env_var_name
from common.proto.context_pb2_grpc import add_ContextServiceServicer_to_server
from common.proto.service_pb2_grpc import add_ServiceServiceServicer_to_server
from common.proto.slice_pb2_grpc import add_SliceServiceServicer_to_server
from common.tests.MockServicerImpl_Context import MockServicerImpl_Context
from common.tests.MockServicerImpl_Service import MockServicerImpl_Service
from common.tests.MockServicerImpl_Slice import MockServicerImpl_Slice
from common.tools.service.GenericGrpcService import GenericGrpcService
LOCAL_HOST = '127.0.0.1'
SERVICE_CONTEXT = ServiceNameEnum.CONTEXT
SERVICE_SERVICE = ServiceNameEnum.SERVICE
SERVICE_SLICE = ServiceNameEnum.SLICE
class MockService_Dependencies(GenericGrpcService):
# Mock Service implementing Context, Service and Slice to simplify unitary tests of NBI
def __init__(self, bind_port: Union[str, int]) -> None:
super().__init__(bind_port, LOCAL_HOST, enable_health_servicer=False, cls_name='MockService')
# 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)
self.slice_servicer = MockServicerImpl_Slice()
add_SliceServiceServicer_to_server(self.slice_servicer, self.server)
def configure_env_vars(self):
os.environ[get_env_var_name(SERVICE_CONTEXT, ENVVAR_SUFIX_SERVICE_HOST )] = str(self.bind_address)
os.environ[get_env_var_name(SERVICE_CONTEXT, ENVVAR_SUFIX_SERVICE_PORT_GRPC)] = str(self.bind_port)
os.environ[get_env_var_name(SERVICE_SERVICE, ENVVAR_SUFIX_SERVICE_HOST )] = str(self.bind_address)
os.environ[get_env_var_name(SERVICE_SERVICE, ENVVAR_SUFIX_SERVICE_PORT_GRPC)] = str(self.bind_port)
os.environ[get_env_var_name(SERVICE_SLICE, ENVVAR_SUFIX_SERVICE_HOST )] = str(self.bind_address)
os.environ[get_env_var_name(SERVICE_SLICE, ENVVAR_SUFIX_SERVICE_PORT_GRPC)] = str(self.bind_port)
# Copyright 2022-2024 ETSI SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/)
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import enum, logging, os, pytest, requests, time
from typing import Any, Dict, List, Optional, Set, Union
from common.Constants import ServiceNameEnum
from common.Settings import (
ENVVAR_SUFIX_SERVICE_HOST, ENVVAR_SUFIX_SERVICE_PORT_HTTP,
get_env_var_name, get_service_baseurl_http, get_service_port_http
)
from context.client.ContextClient import ContextClient
from nbi.service.rest_server.RestServer import RestServer
from nbi.service.rest_server.nbi_plugins.etsi_bwm import register_etsi_bwm_api
from nbi.service.rest_server.nbi_plugins.ietf_l2vpn import register_ietf_l2vpn
from nbi.service.rest_server.nbi_plugins.ietf_l3vpn import register_ietf_l3vpn
from nbi.service.rest_server.nbi_plugins.ietf_network import register_ietf_network
from nbi.service.rest_server.nbi_plugins.tfs_api import register_tfs_api
from nbi.tests.MockService_Dependencies import MockService_Dependencies
from service.client.ServiceClient import ServiceClient
from slice.client.SliceClient import SliceClient
from tests.tools.mock_osm.MockOSM import MockOSM
from .Constants import USERNAME, PASSWORD, WIM_MAPPING
LOCAL_HOST = '127.0.0.1'
MOCKSERVICE_PORT = 10000
NBI_SERVICE_PORT = MOCKSERVICE_PORT + get_service_port_http(ServiceNameEnum.NBI) # avoid privileged ports
os.environ[get_env_var_name(ServiceNameEnum.NBI, ENVVAR_SUFIX_SERVICE_HOST )] = str(LOCAL_HOST)
os.environ[get_env_var_name(ServiceNameEnum.NBI, ENVVAR_SUFIX_SERVICE_PORT_HTTP)] = str(NBI_SERVICE_PORT)
@pytest.fixture(scope='session')
def mock_service():
_service = MockService_Dependencies(MOCKSERVICE_PORT)
_service.configure_env_vars()
_service.start()
yield _service
_service.stop()
@pytest.fixture(scope='session')
def nbi_service_rest(mock_service : MockService_Dependencies): # pylint: disable=redefined-outer-name, unused-argument
_rest_server = RestServer()
register_etsi_bwm_api(_rest_server)
register_ietf_l2vpn(_rest_server)
register_ietf_l3vpn(_rest_server)
register_ietf_network(_rest_server)
register_tfs_api(_rest_server)
_rest_server.start()
time.sleep(1) # bring time for the server to start
yield _rest_server
_rest_server.shutdown()
_rest_server.join()
@pytest.fixture(scope='session')
def osm_wim(nbi_service_rest : RestServer): # pylint: disable=redefined-outer-name, unused-argument
wim_url = 'http://{:s}:{:d}'.format(LOCAL_HOST, NBI_SERVICE_PORT)
return MockOSM(wim_url, WIM_MAPPING, USERNAME, PASSWORD)
@pytest.fixture(scope='session')
def context_client(mock_service : MockService_Dependencies): # pylint: disable=redefined-outer-name, unused-argument
_client = ContextClient()
yield _client
_client.close()
@pytest.fixture(scope='session')
def service_client(mock_service : MockService_Dependencies): # pylint: disable=redefined-outer-name, unused-argument
_client = ServiceClient()
yield _client
_client.close()
@pytest.fixture(scope='session')
def slice_client(mock_service : MockService_Dependencies): # pylint: disable=redefined-outer-name, unused-argument
_client = SliceClient()
yield _client
_client.close()
class RestRequestMethod(enum.Enum):
GET = 'get'
POST = 'post'
PUT = 'put'
PATCH = 'patch'
DELETE = 'delete'
EXPECTED_STATUS_CODES : Set[int] = {
requests.codes['OK' ],
requests.codes['CREATED' ],
requests.codes['ACCEPTED' ],
requests.codes['NO_CONTENT'],
}
def do_rest_request(
method : RestRequestMethod, url : str, body : Optional[Any] = None, timeout : int = 10,
allow_redirects : bool = True, expected_status_codes : Set[int] = EXPECTED_STATUS_CODES,
logger : Optional[logging.Logger] = None
) -> Optional[Union[Dict, List]]:
base_url = get_service_baseurl_http(ServiceNameEnum.NBI) or ''
request_url = 'http://{:s}:{:s}@{:s}:{:d}{:s}{:s}'.format(
USERNAME, PASSWORD, LOCAL_HOST, NBI_SERVICE_PORT, str(base_url), url
)
if logger is not None:
msg = 'Request: {:s} {:s}'.format(str(method.value).upper(), str(request_url))
if body is not None: msg += ' body={:s}'.format(str(body))
logger.warning(msg)
reply = requests.request(method.value, request_url, timeout=timeout, json=body, allow_redirects=allow_redirects)
if logger is not None:
logger.warning('Reply: {:s}'.format(str(reply.text)))
assert reply.status_code in expected_status_codes, 'Reply failed with status code {:d}'.format(reply.status_code)
if reply.content and len(reply.content) > 0: return reply.json()
return None
def do_rest_get_request(
url : str, body : Optional[Any] = None, timeout : int = 10,
allow_redirects : bool = True, expected_status_codes : Set[int] = EXPECTED_STATUS_CODES,
logger : Optional[logging.Logger] = None
) -> Optional[Union[Dict, List]]:
return do_rest_request(
RestRequestMethod.GET, url, body=body, timeout=timeout, allow_redirects=allow_redirects,
expected_status_codes=expected_status_codes, logger=logger
)
def do_rest_post_request(
url : str, body : Optional[Any] = None, timeout : int = 10,
allow_redirects : bool = True, expected_status_codes : Set[int] = EXPECTED_STATUS_CODES,
logger : Optional[logging.Logger] = None
) -> Optional[Union[Dict, List]]:
return do_rest_request(
RestRequestMethod.POST, url, body=body, timeout=timeout, allow_redirects=allow_redirects,
expected_status_codes=expected_status_codes, logger=logger
)
def do_rest_put_request(
url : str, body : Optional[Any] = None, timeout : int = 10,
allow_redirects : bool = True, expected_status_codes : Set[int] = EXPECTED_STATUS_CODES,
logger : Optional[logging.Logger] = None
) -> Optional[Union[Dict, List]]:
return do_rest_request(
RestRequestMethod.PUT, url, body=body, timeout=timeout, allow_redirects=allow_redirects,
expected_status_codes=expected_status_codes, logger=logger
)
def do_rest_patch_request(
url : str, body : Optional[Any] = None, timeout : int = 10,
allow_redirects : bool = True, expected_status_codes : Set[int] = EXPECTED_STATUS_CODES,
logger : Optional[logging.Logger] = None
) -> Optional[Union[Dict, List]]:
return do_rest_request(
RestRequestMethod.PATCH, url, body=body, timeout=timeout, allow_redirects=allow_redirects,
expected_status_codes=expected_status_codes, logger=logger
)
def do_rest_delete_request(
url : str, body : Optional[Any] = None, timeout : int = 10,
allow_redirects : bool = True, expected_status_codes : Set[int] = EXPECTED_STATUS_CODES,
logger : Optional[logging.Logger] = None
) -> Optional[Union[Dict, List]]:
return do_rest_request(
RestRequestMethod.DELETE, url, body=body, timeout=timeout, allow_redirects=allow_redirects,
expected_status_codes=expected_status_codes, logger=logger
)
# Copyright 2022-2024 ETSI SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/)
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
{
"ietf-access-control-list": {
"acls": {
"acl": [
{
"name": "sample-ipv4-acl",
"type": "ipv4-acl-type",
"aces": {
"ace": [
{
"name": "rule1",
"matches": {
"ipv4": {
"dscp": 18,
"source-ipv4-network": "128.32.10.6/24",
"destination-ipv4-network": "172.10.33.0/24"
},
"tcp": {
"flags": "syn",
"source-port": {
"port": 1444,
"operator": "eq"
},
"destination-port": {
"port": 1333,
"operator": "eq"
}
}
},
"actions": {
"forwarding": "drop"
}
}
]
}
}
],
"attachment-points": {
"interface": [
{
"interface-id": "200",
"ingress": {
"acl-sets": {
"acl-set": [
{
"name": "sample-ipv4-acl"
}
]
}
}
}
]
}
}
}
}
{
"ietf-l3vpn-svc:l3vpn-svc": {
"vpn-services": {
"vpn-service": [
{
"vpn-id": "vpn1"
}
]
},
"sites": {
"site": [
{
"site-id": "site_OLT",
"management": {
"type": "ietf-l3vpn-svc:provider-managed"
},
"locations": {
"location": [
{
"location-id": "OLT"
}
]
},
"devices": {
"device": [
{
"device-id": "128.32.33.5",
"location": "OLT"
}
]
},
"routing-protocols": {
"routing-protocol": [
{
"type": "ietf-l3vpn-svc:static",
"static": {
"cascaded-lan-prefixes": {
"ipv4-lan-prefixes": [
{
"lan": "128.32.10.1/24",
"lan-tag": "vlan21",
"next-hop": "128.32.33.2"
},
{
"lan": "128.32.20.1/24",
"lan-tag": "vlan21",
"next-hop": "128.32.33.2"
}
]
}
}
}
]
},
"site-network-accesses": {
"site-network-access": [
{
"site-network-access-id": "500",
"site-network-access-type": "ietf-l3vpn-svc:multipoint",
"device-reference": "128.32.33.5",
"vpn-attachment": {
"vpn-id": "vpn1",
"site-role": "ietf-l3vpn-svc:spoke-role"
},
"ip-connection": {
"ipv4": {
"address-allocation-type": "ietf-l3vpn-svc:static-address",
"addresses": {
"provider-address": "128.32.33.254",
"customer-address": "128.32.33.2",
"prefix-length": 24
}
}
},
"routing-protocols": {
"routing-protocol": [
{
"type": "ietf-l3vpn-svc:static",
"static": {
"cascaded-lan-prefixes": {
"ipv4-lan-prefixes": [
{
"lan": "172.1.101.1/24",
"lan-tag": "vlan21",
"next-hop": "128.32.33.254"
}
]
}
}
}
]
},
"service": {
"svc-mtu": 1500,
"svc-input-bandwidth": 1000000000,
"svc-output-bandwidth": 1000000000,
"qos": {
"qos-profile": {
"classes": {
"class": [
{
"class-id": "qos-realtime",
"direction": "ietf-l3vpn-svc:both",
"latency": {
"latency-boundary": 10
},
"bandwidth": {
"guaranteed-bw-percent": 100
}
}
]
}
}
}
}
}
]
}
},
{
"site-id": "site_POP",
"management": {
"type": "ietf-l3vpn-svc:provider-managed"
},
"locations": {
"location": [
{
"location-id": "POP"
}
]
},
"devices": {
"device": [
{
"device-id": "172.10.33.5",
"location": "POP"
}
]
},
"routing-protocols": {
"routing-protocol": [
{
"type": "ietf-l3vpn-svc:static",
"static": {
"cascaded-lan-prefixes": {
"ipv4-lan-prefixes": [
{
"lan": "172.1.101.1/24",
"lan-tag": "vlan101",
"next-hop": "172.10.33.2"
}
]
}
}
}
]
},
"site-network-accesses": {
"site-network-access": [
{
"site-network-access-id": "500",
"site-network-access-type": "ietf-l3vpn-svc:multipoint",
"device-reference": "172.10.33.5",
"vpn-attachment": {
"vpn-id": "vpn1",
"site-role": "ietf-l3vpn-svc:hub-role"
},
"ip-connection": {
"ipv4": {
"address-allocation-type": "ietf-l3vpn-svc:static-address",
"addresses": {
"provider-address": "172.10.33.254",
"customer-address": "172.10.33.2",
"prefix-length": 24
}
}
},
"routing-protocols": {
"routing-protocol": [
{
"type": "ietf-l3vpn-svc:static",
"static": {
"cascaded-lan-prefixes": {
"ipv4-lan-prefixes": [
{
"lan": "128.32.10.1/24",
"lan-tag": "vlan101",
"next-hop": "172.10.33.254"
},
{
"lan": "128.32.20.1/24",
"lan-tag": "vlan101",
"next-hop": "172.10.33.254"
}
]
}
}
}
]
},
"service": {
"svc-mtu": 1500,
"svc-input-bandwidth": 1000000000,
"svc-output-bandwidth": 1000000000,
"qos": {
"qos-profile": {
"classes": {
"class": [
{
"class-id": "qos-realtime",
"direction": "ietf-l3vpn-svc:both",
"latency": {
"latency-boundary": 10
},
"bandwidth": {
"guaranteed-bw-percent": 100
}
}
]
}
}
}
}
}
]
}
}
]
}
}
}
\ No newline at end of file
{
"ietf-l3vpn-svc:l3vpn-svc": {
"vpn-services": {
"vpn-service": [
{
"vpn-id": "vpn2"
}
]
},
"sites": {
"site": [
{
"site-id": "site_OLT",
"management": {
"type": "ietf-l3vpn-svc:provider-managed"
},
"locations": {
"location": [
{
"location-id": "OLT"
}
]
},
"devices": {
"device": [
{
"device-id": "128.32.33.5",
"location": "OLT"
}
]
},
"routing-protocols": {
"routing-protocol": [
{
"type": "ietf-l3vpn-svc:static",
"static": {
"cascaded-lan-prefixes": {
"ipv4-lan-prefixes": [
{
"lan": "128.32.10.1/24",
"lan-tag": "vlan31",
"next-hop": "128.32.33.2"
},
{
"lan": "128.32.20.1/24",
"lan-tag": "vlan31",
"next-hop": "128.32.33.2"
}
]
}
}
}
]
},
"site-network-accesses": {
"site-network-access": [
{
"site-network-access-id": "500",
"site-network-access-type": "ietf-l3vpn-svc:multipoint",
"device-reference": "128.32.33.5",
"vpn-attachment": {
"vpn-id": "vpn2",
"site-role": "ietf-l3vpn-svc:spoke-role"
},
"ip-connection": {
"ipv4": {
"address-allocation-type": "ietf-l3vpn-svc:static-address",
"addresses": {
"provider-address": "128.32.33.254",
"customer-address": "128.32.33.2",
"prefix-length": 24
}
}
},
"routing-protocols": {
"routing-protocol": [
{
"type": "ietf-l3vpn-svc:static",
"static": {
"cascaded-lan-prefixes": {
"ipv4-lan-prefixes": [
{
"lan": "172.1.201.1/24",
"lan-tag": "vlan31",
"next-hop": "128.32.33.254"
}
]
}
}
}
]
},
"service": {
"svc-mtu": 1500,
"svc-input-bandwidth": 1000000000,
"svc-output-bandwidth": 1000000000,
"qos": {
"qos-profile": {
"classes": {
"class": [
{
"class-id": "qos-realtime",
"direction": "ietf-l3vpn-svc:both",
"latency": {
"latency-boundary": 10
},
"bandwidth": {
"guaranteed-bw-percent": 100
}
}
]
}
}
}
}
}
]
}
},
{
"site-id": "site_POP",
"management": {
"type": "ietf-l3vpn-svc:provider-managed"
},
"locations": {
"location": [
{
"location-id": "POP"
}
]
},
"devices": {
"device": [
{
"device-id": "172.10.33.5",
"location": "POP"
}
]
},
"routing-protocols": {
"routing-protocol": [
{
"type": "ietf-l3vpn-svc:static",
"static": {
"cascaded-lan-prefixes": {
"ipv4-lan-prefixes": [
{
"lan": "172.1.201.1/24",
"lan-tag": "vlan201",
"next-hop": "172.10.33.2"
}
]
}
}
}
]
},
"site-network-accesses": {
"site-network-access": [
{
"site-network-access-id": "500",
"site-network-access-type": "ietf-l3vpn-svc:multipoint",
"device-reference": "172.10.33.5",
"vpn-attachment": {
"vpn-id": "vpn2",
"site-role": "ietf-l3vpn-svc:hub-role"
},
"ip-connection": {
"ipv4": {
"address-allocation-type": "ietf-l3vpn-svc:static-address",
"addresses": {
"provider-address": "172.10.33.254",
"customer-address": "172.10.33.2",
"prefix-length": 24
}
}
},
"routing-protocols": {
"routing-protocol": [
{
"type": "ietf-l3vpn-svc:static",
"static": {
"cascaded-lan-prefixes": {
"ipv4-lan-prefixes": [
{
"lan": "128.32.10.1/24",
"lan-tag": "vlan201",
"next-hop": "172.10.33.254"
},
{
"lan": "128.32.20.1/24",
"lan-tag": "vlan201",
"next-hop": "172.10.33.254"
}
]
}
}
}
]
},
"service": {
"svc-mtu": 1500,
"svc-input-bandwidth": 1000000000,
"svc-output-bandwidth": 1000000000,
"qos": {
"qos-profile": {
"classes": {
"class": [
{
"class-id": "qos-realtime",
"direction": "ietf-l3vpn-svc:both",
"latency": {
"latency-boundary": 10
},
"bandwidth": {
"guaranteed-bw-percent": 100
}
}
]
}
}
}
}
}
]
}
}
]
}
}
}
\ No newline at end of file
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