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

IETF ACTN Driver:

- Proto: Added to proto files
- Common/type_checkers: added assertions
- Context: Added to database model
- Device: Added driver skeleton
- Policy: Added Serializer
- Service: Added to service handler filter fields
- WebUI: Added to forms and pages
- ZTP: Added Serializer
parent 97bf91d2
No related branches found
No related tags found
2 merge requests!235Release TeraFlowSDN 3.0,!188Resolve "(CTTC) Implement SBI Driver ACTN"
Showing
with 405 additions and 0 deletions
......@@ -202,6 +202,7 @@ enum DeviceDriverEnum {
DEVICEDRIVER_IETF_L2VPN = 7;
DEVICEDRIVER_GNMI_OPENCONFIG = 8;
DEVICEDRIVER_FLEXSCALE = 9;
DEVICEDRIVER_IETF_ACTN = 10;
}
enum DeviceOperationalStatusEnum {
......
......@@ -35,6 +35,8 @@ def validate_device_driver_enum(message):
'DEVICEDRIVER_XR',
'DEVICEDRIVER_IETF_L2VPN',
'DEVICEDRIVER_GNMI_OPENCONFIG',
'DEVICEDRIVER_FLEXSCALE',
'DEVICEDRIVER_IETF_ACTN',
]
def validate_device_operational_status_enum(message):
......
......@@ -32,6 +32,7 @@ class ORM_DeviceDriverEnum(enum.Enum):
IETF_L2VPN = DeviceDriverEnum.DEVICEDRIVER_IETF_L2VPN
GNMI_OPENCONFIG = DeviceDriverEnum.DEVICEDRIVER_GNMI_OPENCONFIG
FLEXSCALE = DeviceDriverEnum.DEVICEDRIVER_FLEXSCALE
IETF_ACTN = DeviceDriverEnum.DEVICEDRIVER_IETF_ACTN
grpc_to_enum__device_driver = functools.partial(
grpc_to_enum, DeviceDriverEnum, ORM_DeviceDriverEnum)
# 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.
import logging, requests, threading
from requests.auth import HTTPBasicAuth
from typing import Any, Iterator, List, Optional, Tuple, Union
from common.method_wrappers.Decorator import MetricsPool, metered_subclass_method
from common.type_checkers.Checkers import chk_string, chk_type
from device.service.driver_api._Driver import _Driver
from . import ALL_RESOURCE_KEYS
#from .Tools import create_connectivity_service, find_key, config_getter, delete_connectivity_service
LOGGER = logging.getLogger(__name__)
DRIVER_NAME = 'ietf_actn'
METRICS_POOL = MetricsPool('Device', 'Driver', labels={'driver': DRIVER_NAME})
class IetfActnDriver(_Driver):
def __init__(self, address: str, port: int, **settings) -> None:
super().__init__(DRIVER_NAME, address, port, **settings)
self.__lock = threading.Lock()
self.__started = threading.Event()
self.__terminate = threading.Event()
username = self.settings.get('username')
password = self.settings.get('password')
self.__auth = HTTPBasicAuth(username, password) if username is not None and password is not None else None
scheme = self.settings.get('scheme', 'http')
self.__base_url = '{:s}://{:s}:{:d}'.format(scheme, self.address, int(self.port))
self.__timeout = int(self.settings.get('timeout', 120))
def Connect(self) -> bool:
#url = self.__base_url + '/restconf/data/tapi-common:context'
#with self.__lock:
# if self.__started.is_set(): return True
# try:
# requests.get(url, timeout=self.__timeout, verify=False, auth=self.__auth)
# except requests.exceptions.Timeout:
# LOGGER.exception('Timeout connecting {:s}'.format(str(self.__base_url)))
# return False
# except Exception: # pylint: disable=broad-except
# LOGGER.exception('Exception connecting {:s}'.format(str(self.__base_url)))
# return False
# else:
# self.__started.set()
return True
def Disconnect(self) -> bool:
with self.__lock:
self.__terminate.set()
return True
@metered_subclass_method(METRICS_POOL)
def GetInitialConfig(self) -> List[Tuple[str, Any]]:
with self.__lock:
return []
@metered_subclass_method(METRICS_POOL)
def GetConfig(self, resource_keys : List[str] = []) -> List[Tuple[str, Union[Any, None, Exception]]]:
chk_type('resources', resource_keys, list)
results = []
#with self.__lock:
# if len(resource_keys) == 0: resource_keys = ALL_RESOURCE_KEYS
# for i, resource_key in enumerate(resource_keys):
# str_resource_name = 'resource_key[#{:d}]'.format(i)
# chk_string(str_resource_name, resource_key, allow_empty=False)
# results.extend(config_getter(
# self.__base_url, resource_key, timeout=self.__timeout, auth=self.__auth))
return results
@metered_subclass_method(METRICS_POOL)
def SetConfig(self, resources: List[Tuple[str, Any]]) -> List[Union[bool, Exception]]:
results = []
if len(resources) == 0:
return results
#with self.__lock:
# for resource in resources:
# LOGGER.info('resource = {:s}'.format(str(resource)))
#
# uuid = find_key(resource, 'uuid')
# input_sip = find_key(resource, 'input_sip_uuid')
# output_sip = find_key(resource, 'output_sip_uuid')
# capacity_value = find_key(resource, 'capacity_value')
# capacity_unit = find_key(resource, 'capacity_unit')
# layer_protocol_name = find_key(resource, 'layer_protocol_name')
# layer_protocol_qualifier = find_key(resource, 'layer_protocol_qualifier')
# direction = find_key(resource, 'direction')
#
# data = create_connectivity_service(
# self.__base_url, uuid, input_sip, output_sip, direction, capacity_value, capacity_unit,
# layer_protocol_name, layer_protocol_qualifier, timeout=self.__timeout, auth=self.__auth)
# results.extend(data)
return results
@metered_subclass_method(METRICS_POOL)
def DeleteConfig(self, resources: List[Tuple[str, Any]]) -> List[Union[bool, Exception]]:
results = []
if len(resources) == 0: return results
#with self.__lock:
# for resource in resources:
# LOGGER.info('resource = {:s}'.format(str(resource)))
# uuid = find_key(resource, 'uuid')
# results.extend(delete_connectivity_service(
# self.__base_url, uuid, timeout=self.__timeout, auth=self.__auth))
return results
@metered_subclass_method(METRICS_POOL)
def SubscribeState(self, subscriptions : List[Tuple[str, float, float]]) -> List[Union[bool, Exception]]:
# TODO: IETF ACTN does not support monitoring by now
return [False for _ in subscriptions]
@metered_subclass_method(METRICS_POOL)
def UnsubscribeState(self, subscriptions : List[Tuple[str, float, float]]) -> List[Union[bool, Exception]]:
# TODO: IETF ACTN does not support monitoring by now
return [False for _ in subscriptions]
def GetState(
self, blocking=False, terminate : Optional[threading.Event] = None
) -> Iterator[Tuple[float, str, Any]]:
# TODO: IETF ACTN does not support monitoring by now
return []
# 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.
import json, logging, operator, requests
from requests.auth import HTTPBasicAuth
from typing import Optional
from device.service.driver_api._Driver import RESOURCE_ENDPOINTS, RESOURCE_SERVICES
LOGGER = logging.getLogger(__name__)
HTTP_OK_CODES = {
200, # OK
201, # Created
202, # Accepted
204, # No Content
}
def find_key(resource, key):
return json.loads(resource[1])[key]
def config_getter(
root_url : str, resource_key : str, auth : Optional[HTTPBasicAuth] = None, timeout : Optional[int] = None
):
url = '{:s}/restconf/data/tapi-common:context'.format(root_url)
result = []
try:
response = requests.get(url, timeout=timeout, verify=False, auth=auth)
except requests.exceptions.Timeout:
LOGGER.exception('Timeout connecting {:s}'.format(url))
return result
except Exception as e: # pylint: disable=broad-except
LOGGER.exception('Exception retrieving {:s}'.format(resource_key))
result.append((resource_key, e))
return result
try:
context = json.loads(response.content)
except Exception as e: # pylint: disable=broad-except
LOGGER.warning('Unable to decode reply: {:s}'.format(str(response.content)))
result.append((resource_key, e))
return result
if resource_key == RESOURCE_ENDPOINTS:
if 'tapi-common:context' in context:
context = context['tapi-common:context']
elif 'context' in context:
context = context['context']
for sip in context['service-interface-point']:
layer_protocol_name = sip.get('layer-protocol-name', '?')
supportable_spectrum = sip.get('tapi-photonic-media:media-channel-service-interface-point-spec', {})
supportable_spectrum = supportable_spectrum.get('mc-pool', {})
supportable_spectrum = supportable_spectrum.get('supportable-spectrum', [])
supportable_spectrum = supportable_spectrum[0] if len(supportable_spectrum) == 1 else {}
grid_type = supportable_spectrum.get('frequency-constraint', {}).get('grid-type')
granularity = supportable_spectrum.get('frequency-constraint', {}).get('adjustment-granularity')
direction = sip.get('direction', '?')
endpoint_type = [layer_protocol_name, grid_type, granularity, direction]
str_endpoint_type = ':'.join(filter(lambda i: operator.is_not(i, None), endpoint_type))
sip_uuid = sip['uuid']
sip_names = sip.get('name', [])
sip_name = next(iter([
sip_name['value']
for sip_name in sip_names
if sip_name['value-name'] == 'local-name'
]), sip_uuid)
endpoint_url = '/endpoints/endpoint[{:s}]'.format(sip_uuid)
endpoint_data = {'uuid': sip_uuid, 'name': sip_name, 'type': str_endpoint_type}
result.append((endpoint_url, endpoint_data))
elif resource_key == RESOURCE_SERVICES:
if 'tapi-common:context' in context:
context = context['tapi-common:context']
elif 'context' in context:
context = context['context']
if 'tapi-connectivity:connectivity-context' in context:
context = context['tapi-connectivity:connectivity-context']
elif 'connectivity-context' in context:
context = context['connectivity-context']
for conn_svc in context['connectivity-service']:
service_uuid = conn_svc['uuid']
constraints = conn_svc.get('connectivity-constraint', {})
total_req_cap = constraints.get('requested-capacity', {}).get('total-size', {})
service_url = '/services/service[{:s}]'.format(service_uuid)
service_data = {
'uuid': service_uuid,
'direction': constraints.get('connectivity-direction', 'UNIDIRECTIONAL'),
'capacity_unit': total_req_cap.get('unit', '<UNDEFINED>'),
'capacity_value': total_req_cap.get('value', '<UNDEFINED>'),
}
for i,endpoint in enumerate(conn_svc.get('end-point', [])):
layer_protocol_name = endpoint.get('layer-protocol-name')
if layer_protocol_name is not None:
service_data['layer_protocol_name'] = layer_protocol_name
layer_protocol_qualifier = endpoint.get('layer-protocol-qualifier')
if layer_protocol_qualifier is not None:
service_data['layer_protocol_qualifier'] = layer_protocol_qualifier
sip = endpoint['service-interface-point']['service-interface-point-uuid']
service_data['input_sip' if i == 0 else 'output_sip'] = sip
result.append((service_url, service_data))
return result
def create_connectivity_service(
root_url, uuid, input_sip, output_sip, direction, capacity_value, capacity_unit, layer_protocol_name,
layer_protocol_qualifier,
auth : Optional[HTTPBasicAuth] = None, timeout : Optional[int] = None
):
url = '{:s}/restconf/data/tapi-common:context/tapi-connectivity:connectivity-context'.format(root_url)
headers = {'content-type': 'application/json'}
data = {
'tapi-connectivity:connectivity-service': [
{
'uuid': uuid,
'connectivity-constraint': {
'requested-capacity': {
'total-size': {
'value': capacity_value,
'unit': capacity_unit
}
},
'connectivity-direction': direction
},
'end-point': [
{
'service-interface-point': {
'service-interface-point-uuid': input_sip
},
'layer-protocol-name': layer_protocol_name,
'layer-protocol-qualifier': layer_protocol_qualifier,
'local-id': input_sip
},
{
'service-interface-point': {
'service-interface-point-uuid': output_sip
},
'layer-protocol-name': layer_protocol_name,
'layer-protocol-qualifier': layer_protocol_qualifier,
'local-id': output_sip
}
]
}
]
}
results = []
try:
LOGGER.info('Connectivity service {:s}: {:s}'.format(str(uuid), str(data)))
response = requests.post(
url=url, data=json.dumps(data), timeout=timeout, headers=headers, verify=False, auth=auth)
LOGGER.info('TAPI response: {:s}'.format(str(response)))
except Exception as e: # pylint: disable=broad-except
LOGGER.exception('Exception creating ConnectivityService(uuid={:s}, data={:s})'.format(str(uuid), str(data)))
results.append(e)
else:
if response.status_code not in HTTP_OK_CODES:
msg = 'Could not create ConnectivityService(uuid={:s}, data={:s}). status_code={:s} reply={:s}'
LOGGER.error(msg.format(str(uuid), str(data), str(response.status_code), str(response)))
results.append(response.status_code in HTTP_OK_CODES)
return results
def delete_connectivity_service(root_url, uuid, auth : Optional[HTTPBasicAuth] = None, timeout : Optional[int] = None):
url = '{:s}/restconf/data/tapi-common:context/tapi-connectivity:connectivity-context/connectivity-service={:s}'
url = url.format(root_url, uuid)
results = []
try:
response = requests.delete(url=url, timeout=timeout, verify=False, auth=auth)
except Exception as e: # pylint: disable=broad-except
LOGGER.exception('Exception deleting ConnectivityService(uuid={:s})'.format(str(uuid)))
results.append(e)
else:
if response.status_code not in HTTP_OK_CODES:
msg = 'Could not delete ConnectivityService(uuid={:s}). status_code={:s} reply={:s}'
LOGGER.error(msg.format(str(uuid), str(response.status_code), str(response)))
results.append(response.status_code in HTTP_OK_CODES)
return results
# 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.
from device.service.driver_api._Driver import RESOURCE_ENDPOINTS, RESOURCE_SERVICES
ALL_RESOURCE_KEYS = [
RESOURCE_ENDPOINTS,
RESOURCE_SERVICES,
]
......@@ -2284,6 +2284,12 @@ public class Serializer {
return ContextOuterClass.DeviceDriverEnum.DEVICEDRIVER_XR;
case IETF_L2VPN:
return ContextOuterClass.DeviceDriverEnum.DEVICEDRIVER_IETF_L2VPN;
case GNMI_OPENCONFIG:
return ContextOuterClass.DeviceDriverEnum.DEVICEDRIVER_GNMI_OPENCONFIG;
case FLEXSCALE:
return ContextOuterClass.DeviceDriverEnum.DEVICEDRIVER_FLEXSCALE;
case IETF_ACTN:
return ContextOuterClass.DeviceDriverEnum.DEVICEDRIVER_IETF_ACTN;
case UNDEFINED:
default:
return ContextOuterClass.DeviceDriverEnum.DEVICEDRIVER_UNDEFINED;
......@@ -2307,6 +2313,12 @@ public class Serializer {
return DeviceDriverEnum.XR;
case DEVICEDRIVER_IETF_L2VPN:
return DeviceDriverEnum.IETF_L2VPN;
case DEVICEDRIVER_GNMI_OPENCONFIG:
return DeviceDriverEnum.GNMI_OPENCONFIG;
case DEVICEDRIVER_FLEXSCALE:
return DeviceDriverEnum.FLEXSCALE;
case DEVICEDRIVER_IETF_ACTN:
return DeviceDriverEnum.IETF_ACTN;
case DEVICEDRIVER_UNDEFINED:
case UNRECOGNIZED:
default:
......
......@@ -3614,6 +3614,15 @@ class SerializerTest {
Arguments.of(
DeviceDriverEnum.IETF_L2VPN,
ContextOuterClass.DeviceDriverEnum.DEVICEDRIVER_IETF_L2VPN),
Arguments.of(
DeviceDriverEnum.GNMI_OPENCONFIG,
ContextOuterClass.DeviceDriverEnum.DEVICEDRIVER_GNMI_OPENCONFIG),
Arguments.of(
DeviceDriverEnum.FLEXSCALE,
ContextOuterClass.DeviceDriverEnum.DEVICEDRIVER_FLEXSCALE),
Arguments.of(
DeviceDriverEnum.IETF_ACTN,
ContextOuterClass.DeviceDriverEnum.DEVICEDRIVER_IETF_ACTN),
Arguments.of(
DeviceDriverEnum.UNDEFINED, ContextOuterClass.DeviceDriverEnum.DEVICEDRIVER_UNDEFINED));
}
......
......@@ -39,6 +39,7 @@ DEVICE_DRIVER_VALUES = {
DeviceDriverEnum.DEVICEDRIVER_IETF_L2VPN,
DeviceDriverEnum.DEVICEDRIVER_GNMI_OPENCONFIG,
DeviceDriverEnum.DEVICEDRIVER_FLEXSCALE,
DeviceDriverEnum.DEVICEDRIVER_IETF_ACTN,
}
# Map allowed filter fields to allowed values per Filter field. If no restriction (free text) None is specified
......
......@@ -31,6 +31,8 @@ class AddDeviceForm(FlaskForm):
device_drivers_xr = BooleanField('XR')
device_drivers_ietf_l2vpn = BooleanField('IETF L2VPN')
device_drivers_gnmi_openconfig = BooleanField('GNMI OPENCONFIG')
device_drivers_flexscale = BooleanField('FLEXSCALE')
device_drivers_ietf_actn = BooleanField('IETF ACTN')
device_config_address = StringField('connect/address',default='127.0.0.1',validators=[DataRequired(), Length(min=5)])
device_config_port = StringField('connect/port',default='0',validators=[DataRequired(), Length(min=1)])
......
......@@ -125,6 +125,10 @@ def add():
device_drivers.append(DeviceDriverEnum.DEVICEDRIVER_IETF_L2VPN)
if form.device_drivers_gnmi_openconfig.data:
device_drivers.append(DeviceDriverEnum.DEVICEDRIVER_GNMI_OPENCONFIG)
if form.device_drivers_flexscale.data:
device_drivers.append(DeviceDriverEnum.DEVICEDRIVER_FLEXSCALE)
if form.device_drivers_ietf_actn.data:
device_drivers.append(DeviceDriverEnum.DEVICEDRIVER_IETF_ACTN)
device_obj.device_drivers.extend(device_drivers) # pylint: disable=no-member
try:
......
......@@ -92,6 +92,9 @@
{{ form.device_drivers_xr }} {{ form.device_drivers_xr.label(class="col-sm-3 col-form-label") }}
{{ form.device_drivers_ietf_l2vpn }} {{ form.device_drivers_ietf_l2vpn.label(class="col-sm-3 col-form-label") }}
{{ form.device_drivers_gnmi_openconfig }} {{ form.device_drivers_gnmi_openconfig.label(class="col-sm-3 col-form-label") }}
<br />
{{ form.device_drivers_flexscale }} {{ form.device_drivers_flexscale.label(class="col-sm-3 col-form-label") }}
{{ form.device_drivers_ietf_actn }} {{ form.device_drivers_ietf_actn.label(class="col-sm-3 col-form-label") }}
{% endif %}
</div>
</div>
......
......@@ -863,6 +863,12 @@ public class Serializer {
return ContextOuterClass.DeviceDriverEnum.DEVICEDRIVER_XR;
case IETF_L2VPN:
return ContextOuterClass.DeviceDriverEnum.DEVICEDRIVER_IETF_L2VPN;
case GNMI_OPENCONFIG:
return ContextOuterClass.DeviceDriverEnum.DEVICEDRIVER_GNMI_OPENCONFIG;
case FLEXSCALE:
return ContextOuterClass.DeviceDriverEnum.DEVICEDRIVER_FLEXSCALE;
case IETF_ACTN:
return ContextOuterClass.DeviceDriverEnum.DEVICEDRIVER_IETF_ACTN;
case UNDEFINED:
default:
return ContextOuterClass.DeviceDriverEnum.DEVICEDRIVER_UNDEFINED;
......@@ -886,6 +892,12 @@ public class Serializer {
return DeviceDriverEnum.XR;
case DEVICEDRIVER_IETF_L2VPN:
return DeviceDriverEnum.IETF_L2VPN;
case DEVICEDRIVER_GNMI_OPENCONFIG:
return DeviceDriverEnum.GNMI_OPENCONFIG;
case DEVICEDRIVER_FLEXSCALE:
return DeviceDriverEnum.FLEXSCALE;
case DEVICEDRIVER_IETF_ACTN:
return DeviceDriverEnum.IETF_ACTN;
case DEVICEDRIVER_UNDEFINED:
case UNRECOGNIZED:
default:
......
......@@ -1223,6 +1223,15 @@ class SerializerTest {
Arguments.of(
DeviceDriverEnum.IETF_L2VPN,
ContextOuterClass.DeviceDriverEnum.DEVICEDRIVER_IETF_L2VPN),
Arguments.of(
DeviceDriverEnum.GNMI_OPENCONFIG,
ContextOuterClass.DeviceDriverEnum.DEVICEDRIVER_GNMI_OPENCONFIG),
Arguments.of(
DeviceDriverEnum.FLEXSCALE,
ContextOuterClass.DeviceDriverEnum.DEVICEDRIVER_FLEXSCALE),
Arguments.of(
DeviceDriverEnum.IETF_ACTN,
ContextOuterClass.DeviceDriverEnum.DEVICEDRIVER_IETF_ACTN),
Arguments.of(
DeviceDriverEnum.UNDEFINED, ContextOuterClass.DeviceDriverEnum.DEVICEDRIVER_UNDEFINED));
}
......
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