Commit fcd3d2f7 authored by Lluis Gifre Renom's avatar Lluis Gifre Renom
Browse files

Device - IETF ACTN Driver:

- Implemented EthtServiceHandler (partial, missing get)
- Implemented OsuTunnelHandler (partial, missing get)
- Implemented common RestApiClient
parent 9c83759f
Loading
Loading
Loading
Loading
+0 −3
Original line number Diff line number Diff line
@@ -13,7 +13,6 @@
# limitations under the License.

import json, 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
@@ -27,8 +26,6 @@ LOGGER = logging.getLogger(__name__)
DRIVER_NAME = 'ietf_actn'
METRICS_POOL = MetricsPool('Device', 'Driver', labels={'driver': DRIVER_NAME})

DEFAULT_BASE_URL = '/restconf/data'
DEFAULT_TIMEOUT = 120

class IetfActnDriver(_Driver):
    def __init__(self, address: str, port: int, **settings) -> None:
+43 −3
Original line number Diff line number Diff line
@@ -12,10 +12,12 @@
# See the License for the specific language governing permissions and
# limitations under the License.

import enum
from typing import Dict, List, Tuple
import enum, json, logging, operator, requests
from typing import Any, Dict, List, Tuple, Union
from device.service.driver_api._Driver import RESOURCE_ENDPOINTS, RESOURCE_SERVICES
from .RestApiClient import RestApiClient

OSU_TUNNEL_URL = '/restconf/data/ietf-te:tunnel'
LOGGER = logging.getLogger(__name__)

class BandwidthProfileTypeEnum(enum.Enum):
    MEF_10_BWP = 'ietf-eth-tran-types:mef-10-bwp'
@@ -104,3 +106,41 @@ def compose_etht_service(
        'svc-tunnel': [{'tunnel-name': osu_tunnel_name}],
        'optimizations': compose_optimizations(),
    }]}}

class EthtServiceHandler:
    def __init__(self, rest_api_client : RestApiClient) -> None:
        self._rest_api_client = rest_api_client
        self._object_name     = 'EthtService'
        self._subpath_url     = '/ietf-eth-tran-service:etht-svc/etht-svc-instances'

    def get(self) -> List[Tuple[str, Any]]:
        pass

    def update(self, parameters : Dict) -> List[Union[bool, Exception]]:
        name              = parameters['name'           ]
        service_type      = parameters['service_type'   ]
        osu_tunnel_name   = parameters['osu_tunnel_name']

        src_node_id       = parameters['src_node_id'    ]
        src_tp_id         = parameters['src_tp_id'      ]
        src_vlan_tag      = parameters['src_vlan_tag'   ]
        src_static_routes = parameters.get('src_static_routes', [])

        dst_node_id       = parameters['dst_node_id'    ]
        dst_tp_id         = parameters['dst_tp_id'      ]
        dst_vlan_tag      = parameters['dst_vlan_tag'   ]
        dst_static_routes = parameters.get('dst_static_routes', [])

        service_type = ServiceTypeEnum._value2member_map_[service_type]

        data = compose_etht_service(
            name, service_type, osu_tunnel_name,
            src_node_id, src_tp_id, src_vlan_tag, dst_node_id, dst_tp_id, dst_vlan_tag,
            src_static_routes=src_static_routes, dst_static_routes=dst_static_routes
        )

        return self._rest_api_client.update(self._object_name, self._subpath_url, data)

    def delete(self, etht_service_name : str) -> List[Union[bool, Exception]]:
        filters = [('etht-svc-name', etht_service_name)]
        return self._rest_api_client.delete(self._object_name, self._subpath_url, filters)
+31 −87
Original line number Diff line number Diff line
@@ -13,15 +13,12 @@
# limitations under the License.

import enum, json, logging, operator, requests
from requests.auth import HTTPBasicAuth
from typing import Dict, List, Optional
from typing import Any, Dict, List, Tuple, Union
from device.service.driver_api._Driver import RESOURCE_ENDPOINTS, RESOURCE_SERVICES
from .Tools import HTTP_OK_CODES
from .RestApiClient import RestApiClient

LOGGER = logging.getLogger(__name__)

BASE_URL_OSU_TUNNEL = '{:s}/ietf-te:tunnel'

class EndpointProtectionRoleEnum(enum.Enum):
    WORK = 'work'

@@ -84,11 +81,14 @@ def compose_osu_tunnel(
        'protection': compose_osu_tunnel_protection(),
    }]}

class OsuTunnel:
    def __init__(self, base_url : str, auth : Optional[HTTPBasicAuth] = None, timeout : Optional[int] = None) -> None:
        self._base_url = base_url
        self._auth     = auth
        self._timeout  = timeout
class OsuTunnelHandler:
    def __init__(self, rest_api_client : RestApiClient) -> None:
        self._rest_api_client = rest_api_client
        self._object_name     = 'OsuTunnel'
        self._subpath_url     = '/ietf-te:tunnel'

    def get(self) -> List[Tuple[str, Any]]:
        pass

    def get(self, resource_key : str) -> None:
        url = '{:s}/restconf/data/tapi-common:context'.format(base_url)
@@ -110,38 +110,7 @@ class OsuTunnel:
            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 resource_key == RESOURCE_SERVICES:
            if 'tapi-common:context' in context:
                context = context['tapi-common:context']
            elif 'context' in context:
@@ -181,56 +150,31 @@ class OsuTunnel:

        return result

    def update(self, resource_value : Dict) -> None:
        name                 = resource_value['name'                ]
        src_node_id          = resource_value['src_node_id'         ]
        src_tp_id            = resource_value['src_tp_id'           ]
        src_ttp_channel_name = resource_value['src_ttp_channel_name']
        dst_node_id          = resource_value['dst_node_id'         ]
        dst_tp_id            = resource_value['dst_tp_id'           ]
        dst_ttp_channel_name = resource_value['dst_ttp_channel_name']
        odu_type             = resource_value.get('odu_type',       OduTypeEnum.OSUFLEX.value)
        osuflex_number       = resource_value.get('osuflex_number', 1                        )
        delay                = resource_value.get('delay',          20                       )
        bidirectional        = resource_value.get('bidirectional',  True                     )
    def update(self, parameters : Dict) -> List[Union[bool, Exception]]:
        name                 = parameters['name'                ]

        src_node_id          = parameters['src_node_id'         ]
        src_tp_id            = parameters['src_tp_id'           ]
        src_ttp_channel_name = parameters['src_ttp_channel_name']

        dst_node_id          = parameters['dst_node_id'         ]
        dst_tp_id            = parameters['dst_tp_id'           ]
        dst_ttp_channel_name = parameters['dst_ttp_channel_name']

        odu_type             = parameters.get('odu_type',       OduTypeEnum.OSUFLEX.value)
        osuflex_number       = parameters.get('osuflex_number', 1                        )
        delay                = parameters.get('delay',          20                       )
        bidirectional        = parameters.get('bidirectional',  True                     )

        odu_type = OduTypeEnum._value2member_map_[odu_type]

        headers = {'content-type': 'application/json'}
        data = compose_osu_tunnel(
            name, src_node_id, src_tp_id, src_ttp_channel_name, dst_node_id, dst_tp_id, dst_ttp_channel_name,
            odu_type, osuflex_number, delay, bidirectional=bidirectional
        )

        results = []
        try:
            LOGGER.info('OSU Tunnel {:s}: {:s}'.format(str(name), str(data)))
            response = requests.post(
                self._base_url, data=json.dumps(data), timeout=self._timeout,
                headers=headers, verify=False, auth=self._auth
            )
            LOGGER.info('Response: {:s}'.format(str(response)))
        except Exception as e:  # pylint: disable=broad-except
            LOGGER.exception('Exception creating OsuTunnel(name={:s}, data={:s})'.format(str(name), str(data)))
            results.append(e)
        else:
            if response.status_code not in HTTP_OK_CODES:
                msg = 'Could not create OsuTunnel(name={:s}, data={:s}). status_code={:s} reply={:s}'
                LOGGER.error(msg.format(str(name), str(data), str(response.status_code), str(response)))
            results.append(response.status_code in HTTP_OK_CODES)
        return results

    def delete(self, osu_tunnel_name : str) -> List[]:
        url = '{:s}[name={:s}]'.format(self._base_url, osu_tunnel_name)
        results = []
        try:
            response = requests.delete(url=url, timeout=self._timeout, verify=False, auth=self._auth)
        except Exception as e:  # pylint: disable=broad-except
            LOGGER.exception('Exception deleting OsuTunnel(name={:s})'.format(str(osu_tunnel_name)))
            results.append(e)
        else:
            if response.status_code not in HTTP_OK_CODES:
                msg = 'Could not delete OsuTunnel(name={:s}). status_code={:s} reply={:s}'
                LOGGER.error(msg.format(str(osu_tunnel_name), str(response.status_code), str(response)))
            results.append(response.status_code in HTTP_OK_CODES)
        return results
        return self._rest_api_client.update(self._object_name, self._subpath_url, data)

    def delete(self, osu_tunnel_name : str) -> List[Union[bool, Exception]]:
        filters = [('name', osu_tunnel_name)]
        return self._rest_api_client.delete(self._object_name, self._subpath_url, filters)
+143 −0
Original line number Diff line number Diff line
# 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 copy, json, logging, requests
from requests.auth import HTTPBasicAuth
from typing import Any, Dict, List, Tuple, Union

LOGGER = logging.getLogger(__name__)

DEFAULT_BASE_URL = '/restconf/data'
DEFAULT_SCHEMA   = 'http'
DEFAULT_TIMEOUT  = 120
DEFAULT_VERIFY   = False

HTTP_OK_CODES = {
    200,    # OK
    201,    # Created
    202,    # Accepted
    204,    # No Content
}

class RestApiClient:
    def __init__(self, address : str, port : int, settings : Dict[str, Any] = dict()) -> None:
        username = settings.get('username')
        password = settings.get('password')
        self._auth = HTTPBasicAuth(username, password) if username is not None and password is not None else None

        scheme   = settings.get('scheme',   DEFAULT_SCHEMA  )
        base_url = settings.get('base_url', DEFAULT_BASE_URL)
        self._base_url = '{:s}://{:s}:{:d}{:s}'.format(scheme, address, int(port), base_url)

        self._timeout = int(settings.get('timeout', DEFAULT_TIMEOUT))
        self._verify  = int(settings.get('verify',  DEFAULT_VERIFY ))


    def get(
        self, object_name : str, url : str, filters : List[Tuple[str, str]]
    ) -> List[Union[Any, Exception]]:
        str_filters = ''.join([
            '[{:s}={:s}]'.format(filter_field, filter_value)
            for filter_field, filter_value in filters
        ])

        results = []
        try:
            MSG = 'Get {:s}({:s})'
            LOGGER.info(MSG.format(str(object_name), str(str_filters)))
            response = requests.get(
                self._base_url + url + str_filters,
                timeout=self._timeout, verify=self._verify, auth=self._auth
            )
            LOGGER.info('  Response: {:s}'.format(str(response)))
        except Exception as e:  # pylint: disable=broad-except
            MSG = 'Exception Getting {:s}({:s})'
            LOGGER.exception(MSG.format(str(object_name), str(str_filters)))
            results.append(e)
            return results
        else:
            if response.status_code not in HTTP_OK_CODES:
                MSG = 'Could not get {:s}({:s}): status_code={:s} reply={:s}'
                msg = MSG.format(str(object_name), str(str_filters), str(response.status_code), str(response))
                LOGGER.error(msg)
                results.append(Exception(msg))
                return results

        try:
            results.append(json.loads(response.content))
        except Exception:  # pylint: disable=broad-except
            MSG = 'Could not decode reply {:s}({:s}): {:s} {:s}'
            msg = MSG.format(str(object_name), str(str_filters), str(response.status_code), str(response))
            LOGGER.exception(msg)
            results.append(Exception(msg))

        return results

    def update(
        self, object_name : str, url : str, data : Dict, headers : Dict[str, Any] = dict()
    ) -> List[Union[bool, Exception]]:
        headers = copy.deepcopy(headers)
        if 'content-type' not in {header_name.lower() for header_name in headers.keys()}:
            headers.update({'content-type': 'application/json'})

        results = []
        try:
            MSG = 'Create/Update {:s}({:s})'
            LOGGER.info(MSG.format(str(object_name), str(data)))
            response = requests.post(
                self._base_url + url, data=json.dumps(data), headers=headers,
                timeout=self._timeout, verify=self._verify, auth=self._auth
            )
            LOGGER.info('  Response: {:s}'.format(str(response)))
        except Exception as e:  # pylint: disable=broad-except
            MSG = 'Exception Creating/Updating {:s}({:s})'
            LOGGER.exception(MSG.format(str(object_name), str(data)))
            results.append(e)
        else:
            if response.status_code not in HTTP_OK_CODES:
                MSG = 'Could not create/update {:s}({:s}): status_code={:s} reply={:s}'
                LOGGER.error(MSG.format(str(object_name), str(data), str(response.status_code), str(response)))
            results.append(response.status_code in HTTP_OK_CODES)

        return results


    def delete(
        self, object_name : str, url : str, filters : List[Tuple[str, str]]
    ) -> List[Union[bool, Exception]]:
        str_filters = ''.join([
            '[{:s}={:s}]'.format(filter_field, filter_value)
            for filter_field, filter_value in filters
        ])

        results = []
        try:
            MSG = 'Delete {:s}({:s})'
            LOGGER.info(MSG.format(str(object_name), str(str_filters)))
            response = requests.delete(
                self._base_url + url + str_filters,
                timeout=self._timeout, verify=self._verify, auth=self._auth
            )
            LOGGER.info('  Response: {:s}'.format(str(response)))
        except Exception as e:  # pylint: disable=broad-except
            MSG = 'Exception Deleting {:s}({:s})'
            LOGGER.exception(MSG.format(str(object_name), str(str_filters)))
            results.append(e)
        else:
            if response.status_code not in HTTP_OK_CODES:
                MSG = 'Could not delete {:s}({:s}): status_code={:s} reply={:s}'
                LOGGER.error(MSG.format(str(object_name), str(str_filters), str(response.status_code), str(response)))
            results.append(response.status_code in HTTP_OK_CODES)

        return results
+0 −20
Original line number Diff line number Diff line
# 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.

HTTP_OK_CODES = {
    200,    # OK
    201,    # Created
    202,    # Accepted
    204,    # No Content
}