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

Common and Context changes:

- added helper methods to copy/update grpc messages: config_rules, constraints, endpoint_ids, service_ids
-  extended Context ConstraintModel to support different types of constraints
- extended EndpointModel with get_endpoint helper method
- extended SliceModel to incorporate config rules
parent 8a971c07
Loading
Loading
Loading
Loading
+62 −0
Original line number Diff line number Diff line
# Copyright 2021-2023 H2020 TeraFlow (https://www.teraflow-h2020.eu/)
#
# 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.

# RFC 8466 - L2VPN Service Model (L2SM)
# Ref: https://datatracker.ietf.org/doc/html/rfc8466


import json
from typing import Any, Dict, Tuple
from common.proto.context_pb2 import ConfigActionEnum, ConfigRule
from common.tools.grpc.Tools import grpc_message_to_json_string

def update_config_rule_custom(config_rules, resource_key : str, fields : Dict[str, Tuple[Any, bool]]) -> ConfigRule:
    # fields: Dict[field_name : str, Tuple[field_value : Any, raise_if_differs : bool]]

    for config_rule in config_rules:
        if config_rule.WhichOneof('config_rule') != 'custom': continue
        if config_rule.custom.resource_key != resource_key: continue
        json_resource_value = json.loads(config_rule.custom.resource_value)
        break   # found, end loop
    else:
        # not found, add it
        config_rule = config_rules.add()    # pylint: disable=no-member
        config_rule.action = ConfigActionEnum.CONFIGACTION_SET
        config_rule.custom.resource_key = resource_key
        json_resource_value = {}

    for field_name,(field_value, raise_if_differs) in fields.items():
        if (field_name not in json_resource_value) or not raise_if_differs:
            # missing or raise_if_differs=False, add/update it
            json_resource_value[field_name] = field_value
        elif json_resource_value[field_name] != field_value:
            # exists, differs, and raise_if_differs=True
            msg = 'Specified {:s}({:s}) differs existing value({:s})'
            raise Exception(msg.format(str(field_name), str(field_value), str(json_resource_value[field_name])))

    config_rule.custom.resource_value = json.dumps(json_resource_value, sort_keys=True)

def copy_config_rules(source_config_rules, target_config_rules):
    for source_config_rule in source_config_rules:
        config_rule_kind = source_config_rule.WhichOneof('config_rule')
        if config_rule_kind == 'custom':
            custom = source_config_rule.custom
            resource_key = custom.resource_key
            resource_value = json.loads(custom.resource_value)
            raise_if_differs = True
            fields = {name:(value, raise_if_differs) for name,value in resource_value.items()}
            update_config_rule_custom(target_config_rules, resource_key, fields)

        else:
            raise NotImplementedError('ConfigRule({:s})'.format(grpc_message_to_json_string(source_config_rule)))
+164 −0
Original line number Diff line number Diff line
# Copyright 2021-2023 H2020 TeraFlow (https://www.teraflow-h2020.eu/)
#
# 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.

# RFC 8466 - L2VPN Service Model (L2SM)
# Ref: https://datatracker.ietf.org/doc/html/rfc8466


import json
from typing import Any, Dict, Optional, Tuple
from common.proto.context_pb2 import Constraint, EndPointId
from common.tools.grpc.Tools import grpc_message_to_json_string

def update_constraint_custom(constraints, constraint_type : str, fields : Dict[str, Tuple[Any, bool]]) -> Constraint:
    # fields: Dict[field_name : str, Tuple[field_value : Any, raise_if_differs : bool]]

    for constraint in constraints:
        if constraint.WhichOneof('constraint') != 'custom': continue
        if constraint.custom.constraint_type != constraint_type: continue
        json_constraint_value = json.loads(constraint.custom.constraint_value)
        break   # found, end loop
    else:
        # not found, add it
        constraint = constraints.add()      # pylint: disable=no-member
        constraint.custom.constraint_type = constraint_type
        json_constraint_value = {}

    for field_name,(field_value, raise_if_differs) in fields.items():
        if (field_name not in json_constraint_value) or not raise_if_differs:
            # missing or raise_if_differs=False, add/update it
            json_constraint_value[field_name] = field_value
        elif json_constraint_value[field_name] != field_value:
            # exists, differs, and raise_if_differs=True
            msg = 'Specified {:s}({:s}) differs existing value({:s})'
            raise Exception(msg.format(str(field_name), str(field_value), str(json_constraint_value[field_name])))

    constraint.custom.constraint_value = json.dumps(json_constraint_value, sort_keys=True)

def update_constraint_endpoint_location(
    constraints, endpoint_id : EndPointId,
    region : Optional[str] = None, gps_position : Optional[Tuple[float, float]] = None
) -> Constraint:
    # gps_position: (latitude, longitude)
    if region is not None and gps_position is not None:
        raise Exception('Only one of region/gps_position can be provided')

    endpoint_uuid = endpoint_id.endpoint_uuid.uuid
    device_uuid = endpoint_id.device_id.device_uuid.uuid
    topology_uuid = endpoint_id.topology_id.topology_uuid.uuid
    context_uuid = endpoint_id.topology_id.context_id.context_uuid.uuid

    for constraint in constraints:
        if constraint.WhichOneof('constraint') != 'endpoint_location': continue
        _endpoint_id = constraint.endpoint_location.endpoint_id
        if _endpoint_id.endpoint_uuid.uuid != endpoint_uuid: continue
        if _endpoint_id.device_id.device_uuid.uuid != device_uuid: continue
        if _endpoint_id.topology_id.topology_uuid.uuid != topology_uuid: continue
        if _endpoint_id.topology_id.context_id.context_uuid.uuid != context_uuid: continue
        break   # found, end loop
    else:
        # not found, add it
        constraint = constraints.add()      # pylint: disable=no-member
        _endpoint_id = constraint.endpoint_location.endpoint_id
        _endpoint_id.endpoint_uuid.uuid = endpoint_uuid
        _endpoint_id.device_id.device_uuid.uuid = device_uuid
        _endpoint_id.topology_id.topology_uuid.uuid = topology_uuid
        _endpoint_id.topology_id.context_id.context_uuid.uuid = context_uuid

    location = constraint.endpoint_location.location
    if region is not None:
        location.region = region
    elif gps_position is not None:
        location.gps_position.latitude = gps_position[0]
        location.gps_position.longitude = gps_position[1]
    return constraint

def update_constraint_endpoint_priority(constraints, endpoint_id : EndPointId, priority : int) -> Constraint:
    endpoint_uuid = endpoint_id.endpoint_uuid.uuid
    device_uuid = endpoint_id.device_id.device_uuid.uuid
    topology_uuid = endpoint_id.topology_id.topology_uuid.uuid
    context_uuid = endpoint_id.topology_id.context_id.context_uuid.uuid

    for constraint in constraints:
        if constraint.WhichOneof('constraint') != 'endpoint_priority': continue
        _endpoint_id = constraint.endpoint_priority.endpoint_id
        if _endpoint_id.endpoint_uuid.uuid != endpoint_uuid: continue
        if _endpoint_id.device_id.device_uuid.uuid != device_uuid: continue
        if _endpoint_id.topology_id.topology_uuid.uuid != topology_uuid: continue
        if _endpoint_id.topology_id.context_id.context_uuid.uuid != context_uuid: continue
        break   # found, end loop
    else:
        # not found, add it
        constraint = constraints.add()      # pylint: disable=no-member
        _endpoint_id = constraint.endpoint_priority.endpoint_id
        _endpoint_id.endpoint_uuid.uuid = endpoint_uuid
        _endpoint_id.device_id.device_uuid.uuid = device_uuid
        _endpoint_id.topology_id.topology_uuid.uuid = topology_uuid
        _endpoint_id.topology_id.context_id.context_uuid.uuid = context_uuid

    constraint.endpoint_priority.priority = priority
    return constraint

def update_constraint_sla_availability(constraints, num_disjoint_paths : int, all_active : bool) -> Constraint:
    for constraint in constraints:
        if constraint.WhichOneof('constraint') != 'sla_availability': continue
        break   # found, end loop
    else:
        # not found, add it
        constraint = constraints.add()      # pylint: disable=no-member

    constraint.sla_availability.num_disjoint_paths = num_disjoint_paths
    constraint.sla_availability.all_active = all_active
    return constraint


def copy_constraints(source_constraints, target_constraints):
    for source_constraint in source_constraints:
        constraint_kind = source_constraint.WhichOneof('constraint')
        if constraint_kind == 'custom':
            custom = source_constraint.custom
            constraint_type = custom.constraint_type
            constraint_value = json.loads(custom.constraint_value)
            raise_if_differs = True
            fields = {name:(value, raise_if_differs) for name,value in constraint_value.items()}
            update_constraint_custom(target_constraints, constraint_type, fields)

        elif constraint_kind == 'endpoint_location':
            endpoint_id = source_constraint.endpoint_location.endpoint_id
            location = source_constraint.endpoint_location.location
            location_kind = location.WhichOneof('location')
            if location_kind == 'region':
                region = location.region
                update_constraint_endpoint_location(target_constraints, endpoint_id, region=region)
            elif location_kind == 'gps_position':
                gps_position = location.gps_position
                gps_position = (gps_position.latitude, gps_position.longitude)
                update_constraint_endpoint_location(target_constraints, endpoint_id, gps_position=gps_position)
            else:
                str_constraint = grpc_message_to_json_string(source_constraint)
                raise NotImplementedError('Constraint({:s}): Location({:s})'.format(str_constraint, constraint_kind))

        elif constraint_kind == 'endpoint_priority':
            endpoint_id = source_constraint.endpoint_priority.endpoint_id
            priority = source_constraint.endpoint_priority.priority
            update_constraint_endpoint_priority(target_constraints, endpoint_id, priority)

        elif constraint_kind == 'sla_availability':
            sla_availability = source_constraint.sla_availability
            num_disjoint_paths = sla_availability.num_disjoint_paths
            all_active = sla_availability.all_active
            update_constraint_sla_availability(target_constraints, num_disjoint_paths, all_active)

        else:
            raise NotImplementedError('Constraint({:s})'.format(grpc_message_to_json_string(source_constraint)))
+50 −0
Original line number Diff line number Diff line
# Copyright 2021-2023 H2020 TeraFlow (https://www.teraflow-h2020.eu/)
#
# 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.

# RFC 8466 - L2VPN Service Model (L2SM)
# Ref: https://datatracker.ietf.org/doc/html/rfc8466


from typing import Optional
from common.proto.context_pb2 import EndPointId

def update_endpoint_ids(
    endpoint_ids, device_uuid : str, endpoint_uuid : str,
    context_uuid : Optional[str] = None, topology_uuid : Optional[str] = None
) -> EndPointId:
    for endpoint_id in endpoint_ids:
        if endpoint_id.endpoint_uuid.uuid != endpoint_uuid: continue
        if endpoint_id.device_id.device_uuid.uuid != device_uuid: continue
        topology_id = endpoint_id.topology_id
        if topology_uuid is not None and topology_id.topology_uuid.uuid != topology_uuid: continue
        if context_uuid is not None and topology_id.context_id.context_uuid.uuid != context_uuid: continue
        break   # found, do nothing
    else:
        # not found, add it
        endpoint_id = endpoint_ids.add()    # pylint: disable=no-member
        endpoint_id.endpoint_uuid.uuid = endpoint_uuid
        endpoint_id.device_id.device_uuid.uuid = device_uuid
        if topology_uuid is not None: endpoint_id.topology_id.topology_uuid.uuid = topology_uuid
        if context_uuid is not None: endpoint_id.topology_id.context_id.context_uuid.uuid = context_uuid
    return endpoint_id

def copy_endpoint_ids(source_endpoint_ids, target_endpoint_ids):
    for source_endpoint_id in source_endpoint_ids:
        device_uuid = source_endpoint_id.device_id.device_uuid.uuid
        endpoint_uuid = source_endpoint_id.endpoint_uuid.uuid
        source_topology_id = source_endpoint_id.topology_id
        context_uuid = source_topology_id.context_id.context_uuid.uuid
        topology_uuid = source_topology_id.topology_uuid.uuid
        update_endpoint_ids(
            target_endpoint_ids, device_uuid, endpoint_uuid, context_uuid=context_uuid, topology_uuid=topology_uuid)
+37 −0
Original line number Diff line number Diff line
# Copyright 2021-2023 H2020 TeraFlow (https://www.teraflow-h2020.eu/)
#
# 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.

# RFC 8466 - L2VPN Service Model (L2SM)
# Ref: https://datatracker.ietf.org/doc/html/rfc8466


from common.proto.context_pb2 import ServiceId

def update_service_ids(service_ids, context_uuid : str, service_uuid : str) -> ServiceId:
    for service_id in service_ids:
        if service_id.service_uuid.uuid != service_uuid: continue
        if service_id.context_id.context_uuid.uuid != context_uuid: continue
        break   # found, do nothing
    else:
        # not found, add it
        service_id = service_ids.add()    # pylint: disable=no-member
        service_id.service_uuid.uuid = service_uuid
        service_id.context_id.context_uuid.uuid = context_uuid
    return service_id

def copy_service_ids(source_service_ids, target_service_ids):
    for source_service_id in source_service_ids:
        context_uuid = source_service_id.context_id.context_uuid.uuid
        service_uuid = source_service_id.service_uuid.uuid
        update_service_ids(target_service_ids, context_uuid, service_uuid)
+139 −16
Original line number Diff line number Diff line
@@ -13,10 +13,12 @@
# limitations under the License.

import logging, operator
from typing import Dict, List, Tuple, Union
from typing import Dict, List, Tuple, Type, Union
from common.orm.Database import Database
from common.orm.HighLevel import get_or_create_object, update_or_create_object
from common.orm.backend.Tools import key_to_str
from common.orm.fields.BooleanField import BooleanField
from common.orm.fields.FloatField import FloatField
from common.orm.fields.ForeignKeyField import ForeignKeyField
from common.orm.fields.IntegerField import IntegerField
from common.orm.fields.PrimaryKeyField import PrimaryKeyField
@@ -24,7 +26,8 @@ from common.orm.fields.StringField import StringField
from common.orm.model.Model import Model
from common.proto.context_pb2 import Constraint
from common.tools.grpc.Tools import grpc_message_to_json_string
from context.service.database.Tools import fast_hasher, remove_dict_key
from .EndPointModel import EndPointModel, get_endpoint
from .Tools import fast_hasher, remove_dict_key

LOGGER = logging.getLogger(__name__)

@@ -41,6 +44,13 @@ class ConstraintModel(Model): # pylint: disable=abstract-method
    pk = PrimaryKeyField()
    constraints_fk = ForeignKeyField(ConstraintsModel)
    position = IntegerField(min_value=0, required=True)

    def dump(self, include_position=True) -> Dict: # pylint: disable=arguments-differ
        result = {}
        if include_position: result['position'] = self.position
        return result

class ConstraintCustomModel(ConstraintModel): # pylint: disable=abstract-method
    constraint_type = StringField(required=True, allow_empty=False)
    constraint_value = StringField(required=True, allow_empty=False)

@@ -51,28 +61,141 @@ class ConstraintModel(Model): # pylint: disable=abstract-method
                'constraint_value': self.constraint_value,
            },
        }
        if include_position: result['position'] = self.position
        result.update(super().dump(include_position=include_position))
        return result

class ConstraintEndpointLocationModel(ConstraintModel): # pylint: disable=abstract-method
    endpoint_fk = ForeignKeyField(EndPointModel)

    def dump(self, include_position=True) -> Dict: # pylint: disable=arguments-differ
        db_endpoints_pks = list(self.references(EndPointModel))
        num_endpoints = len(db_endpoints_pks)
        if num_endpoints != 1:
            raise Exception('Wrong number({:d}) of associated Endpoints with constraint'.format(num_endpoints))
        db_endpoint = EndPointModel(self.database, db_endpoints_pks[0])
        result = {'endpoint_id': db_endpoint.dump_id()}
        result.update(super().dump(include_position=include_position))
        return result

class ConstraintEndpointLocationRegionModel(ConstraintEndpointLocationModel): # pylint: disable=abstract-method
    region = StringField(required=True, allow_empty=False)

    def dump(self, include_position=True) -> Dict: # pylint: disable=arguments-differ
        result = super().dump(include_position=include_position)
        result['endpoint_location'].setdefault('region', {}).setdefault('region', self.region)
        return result

class ConstraintEndpointLocationGpsPositionModel(ConstraintEndpointLocationModel): # pylint: disable=abstract-method
    latitude = FloatField(required=True, min_value=-90.0, max_value=90.0)
    longitude = FloatField(required=True, min_value=-180.0, max_value=180.0)

    def dump(self, include_position=True) -> Dict: # pylint: disable=arguments-differ
        result = super().dump(include_position=include_position)
        gps_position = result['endpoint_location'].setdefault('gps_position', {})
        gps_position['latitude' ] = self.latitude
        gps_position['longitude'] = self.longitude
        return result

class ConstraintEndpointPriorityModel(ConstraintModel): # pylint: disable=abstract-method
    endpoint_fk = ForeignKeyField(EndPointModel)
    priority = FloatField(required=True)

    def dump(self, include_position=True) -> Dict: # pylint: disable=arguments-differ
        db_endpoints_pks = list(self.references(EndPointModel))
        num_endpoints = len(db_endpoints_pks)
        if num_endpoints != 1:
            raise Exception('Wrong number({:d}) of associated Endpoints with constraint'.format(num_endpoints))
        db_endpoint = EndPointModel(self.database, db_endpoints_pks[0])
        result = {'endpoint_id': db_endpoint.dump_id(), 'priority': self.priority}
        result.update(super().dump(include_position=include_position))
        return result

class ConstraintSlaAvailabilityModel(ConstraintModel): # pylint: disable=abstract-method
    num_disjoint_paths = IntegerField(required=True, min_value=1)
    all_active = BooleanField(required=True)

    def dump(self, include_position=True) -> Dict: # pylint: disable=arguments-differ
        result = {'num_disjoint_paths': self.num_disjoint_paths, 'all_active': self.all_active}
        result.update(super().dump(include_position=include_position))
        return result

def parse_constraint_custom(database : Database, grpc_constraint) -> Tuple[Type, str, Dict]:
    constraint_class = ConstraintCustomModel
    str_constraint_id = grpc_constraint.custom.constraint_type
    constraint_data = {
        'constraint_type' : grpc_constraint.custom.constraint_type,
        'constraint_value': grpc_constraint.custom.constraint_value,
    }
    return constraint_class, str_constraint_id, constraint_data

def parse_constraint_endpoint_location(database : Database, grpc_constraint) -> Tuple[Type, str, Dict]:
    grpc_endpoint_id = grpc_constraint.endpoint_location.endpoint_id
    str_endpoint_key, db_endpoint = get_endpoint(database, grpc_endpoint_id)

    str_constraint_id = str_endpoint_key
    constraint_data = {'endpoint_fk': db_endpoint}

    grpc_location = grpc_constraint.endpoint_location.location
    location_kind = str(grpc_location.WhichOneof('location'))
    if location_kind == 'region':
        constraint_class = ConstraintEndpointLocationRegionModel
        constraint_data.update({'region': grpc_location.region})
    elif location_kind == 'gps_position':
        constraint_class = ConstraintEndpointLocationGpsPositionModel
        gps_position = grpc_location.gps_position
        constraint_data.update({'latitude': gps_position.latitude, 'longitude': gps_position.longitude})
    else:
        MSG = 'Location kind {:s} in Constraint of kind endpoint_location is not implemented: {:s}'
        raise NotImplementedError(MSG.format(location_kind, grpc_message_to_json_string(grpc_constraint)))
    return constraint_class, str_constraint_id, constraint_data

def parse_constraint_endpoint_priority(database : Database, grpc_constraint) -> Tuple[Type, str, Dict]:
    grpc_endpoint_id = grpc_constraint.endpoint_priority.endpoint_id
    str_endpoint_key, db_endpoint = get_endpoint(database, grpc_endpoint_id)

    constraint_class = ConstraintEndpointPriorityModel
    str_constraint_id = str_endpoint_key
    priority = grpc_constraint.endpoint_priority.priority
    constraint_data = {'endpoint_fk': db_endpoint, 'priority': priority}

    return constraint_class, str_constraint_id, constraint_data

def parse_constraint_sla_availability(database : Database, grpc_constraint) -> Tuple[Type, str, Dict]:
    constraint_class = ConstraintSlaAvailabilityModel
    str_constraint_id = ''
    constraint_data = {
        'num_disjoint_paths' : grpc_constraint.sla_availability.num_disjoint_paths,
        'all_active': grpc_constraint.sla_availability.all_active,
    }
    return constraint_class, str_constraint_id, constraint_data

CONSTRAINT_PARSERS = {
    'custom'            : parse_constraint_custom,
    'endpoint_location' : parse_constraint_endpoint_location,
    'endpoint_priority' : parse_constraint_endpoint_priority,
    'sla_availability'  : parse_constraint_sla_availability,
}

def set_constraint(
    database : Database, db_constraints : ConstraintsModel, grpc_constraint, position : int
) -> Tuple[Constraint, bool]:
    constraint_type = str(grpc_constraint.WhichOneof('constraint'))
    if constraint_type != 'custom':
        raise NotImplementedError('Constraint of type {:s} is not implemented: {:s}'.format(
            constraint_type, grpc_message_to_json_string(grpc_constraint)))
    constraint_kind = str(grpc_constraint.WhichOneof('constraint'))

    str_constraint_key_hash = fast_hasher(grpc_constraint.custom.constraint_type)
    parser = CONSTRAINT_PARSERS.get(constraint_kind)
    if parser is None:
        raise NotImplementedError('Constraint of kind {:s} is not implemented: {:s}'.format(
            constraint_kind, grpc_message_to_json_string(grpc_constraint)))

    constraint_class, str_constraint_id, constraint_data = parser(database, grpc_constraint)

    str_constraint_key_hash = fast_hasher(':'.join([constraint_kind, str_constraint_id]))
    str_constraint_key = key_to_str([db_constraints.pk, str_constraint_key_hash], separator=':')
    constraint_data.update({'constraints_fk': db_constraints, 'position': position})

    result : Tuple[ConstraintModel, bool] = update_or_create_object(database, ConstraintModel, str_constraint_key, {
        'constraints_fk'  : db_constraints,
        'position'        : position,
        'constraint_type' : grpc_constraint.custom.constraint_type,
        'constraint_value': grpc_constraint.custom.constraint_value,
    })
    db_config_rule, updated = result
    return db_config_rule, updated
    result : Tuple[ConstraintModel, bool] = update_or_create_object(
        database, constraint_class, str_constraint_key, constraint_data)
    db_constraint, updated = result
    return db_constraint, updated

def set_constraints(
    database : Database, db_parent_pk : str, constraints_name : str, grpc_constraints
Loading