diff --git a/proto/context.proto b/proto/context.proto index 3104f1b545c02bab71c8638ebba03efdcbfe71ff..39abc081d4a652eda246f876c5184faffc7065d5 100644 --- a/proto/context.proto +++ b/proto/context.proto @@ -494,6 +494,12 @@ message ConfigRule { // ----- Constraint ---------------------------------------------------------------------------------------------------- +enum ConstraintActionEnum { + CONSTRAINTACTION_UNDEFINED = 0; + CONSTRAINTACTION_SET = 1; + CONSTRAINTACTION_DELETE = 2; +} + message Constraint_Custom { string constraint_type = 1; string constraint_value = 2; @@ -564,16 +570,17 @@ message Constraint_Exclusions { } message Constraint { + ConstraintActionEnum action = 1; oneof constraint { - Constraint_Custom custom = 1; - Constraint_Schedule schedule = 2; - Constraint_EndPointLocation endpoint_location = 3; - Constraint_EndPointPriority endpoint_priority = 4; - Constraint_SLA_Capacity sla_capacity = 5; - Constraint_SLA_Latency sla_latency = 6; - Constraint_SLA_Availability sla_availability = 7; - Constraint_SLA_Isolation_level sla_isolation = 8; - Constraint_Exclusions exclusions = 9; + Constraint_Custom custom = 2; + Constraint_Schedule schedule = 3; + Constraint_EndPointLocation endpoint_location = 4; + Constraint_EndPointPriority endpoint_priority = 5; + Constraint_SLA_Capacity sla_capacity = 6; + Constraint_SLA_Latency sla_latency = 7; + Constraint_SLA_Availability sla_availability = 8; + Constraint_SLA_Isolation_level sla_isolation = 9; + Constraint_Exclusions exclusions = 10; } } diff --git a/src/common/tests/MockServicerImpl_Context.py b/src/common/tests/MockServicerImpl_Context.py index 0e6f819d8c5e9ddc823b8090a8d4b74548471bd2..3f4af19eced89843552737ae6f8b773d3fa26a58 100644 --- a/src/common/tests/MockServicerImpl_Context.py +++ b/src/common/tests/MockServicerImpl_Context.py @@ -13,15 +13,15 @@ # limitations under the License. import grpc, json, logging -from typing import Any, Dict, Iterator, List +from typing import Any, Dict, Iterator, List, Set from common.proto.context_pb2 import ( Connection, ConnectionEvent, ConnectionId, ConnectionIdList, ConnectionList, Context, ContextEvent, ContextId, ContextIdList, ContextList, - Device, DeviceEvent, DeviceId, DeviceIdList, DeviceList, + Device, DeviceEvent, DeviceFilter, DeviceId, DeviceIdList, DeviceList, Empty, EventTypeEnum, Link, LinkEvent, LinkId, LinkIdList, LinkList, - Service, ServiceEvent, ServiceId, ServiceIdList, ServiceList, - Slice, SliceEvent, SliceId, SliceIdList, SliceList, + Service, ServiceEvent, ServiceFilter, ServiceId, ServiceIdList, ServiceList, + Slice, SliceEvent, SliceFilter, SliceId, SliceIdList, SliceList, Topology, TopologyEvent, TopologyId, TopologyIdList, TopologyList) from common.proto.context_pb2_grpc import ContextServiceServicer from common.tests.MockMessageBroker import ( @@ -68,10 +68,19 @@ def del_entry( del container[entry_uuid] return Empty() +def select_entries(database : Dict[str, Dict[str, Any]], container_name : str, entry_uuids : Set[str]) -> List[Any]: + if len(entry_uuids) == 0: return get_entries(database, container_name) + container = get_container(database, container_name) + return [ + container[entry_uuid] + for entry_uuid in sorted(container.keys()) + if entry_uuid in entry_uuids + ] + class MockServicerImpl_Context(ContextServiceServicer): def __init__(self): LOGGER.info('[__init__] Creating Servicer...') - self.database : Dict[str, Any] = {} + self.database : Dict[str, Dict[str, Any]] = {} self.msg_broker = MockMessageBroker() LOGGER.info('[__init__] Servicer Created') @@ -210,6 +219,33 @@ class MockServicerImpl_Context(ContextServiceServicer): LOGGER.info('[GetDeviceEvents] request={:s}'.format(grpc_message_to_json_string(request))) for message in self.msg_broker.consume({TOPIC_DEVICE}): yield DeviceEvent(**json.loads(message.content)) + def SelectDevice(self, request : DeviceFilter, context : grpc.ServicerContext) -> DeviceList: + LOGGER.info('[SelectDevice] request={:s}'.format(grpc_message_to_json_string(request))) + container_entry_uuids : Dict[str, Set[str]] = {} + container_name = 'device' + for device_id in request.device_ids.device_ids: + device_uuid = device_id.device_uuid.uuid + container_entry_uuids.setdefault(container_name, set()).add(device_uuid) + + exclude_endpoints = not request.include_endpoints + exclude_config_rules = not request.include_config_rules + exclude_components = not request.include_components + + devices = list() + for container_name in sorted(container_entry_uuids.keys()): + entry_uuids = container_entry_uuids[container_name] + for device in select_entries(self.database, container_name, entry_uuids): + reply_device = Device() + reply_device.CopyFrom(device) + if exclude_endpoints: del reply_device.device_endpoints [:] # pylint: disable=no-member + if exclude_config_rules: del reply_device.device_config.config_rules[:] # pylint: disable=no-member + if exclude_components: del reply_device.component[:] # pylint: disable=no-member + devices.append(reply_device) + + reply = DeviceList(devices=devices) + LOGGER.info('[SelectDevice] reply={:s}'.format(grpc_message_to_json_string(reply))) + return reply + # ----- Link ------------------------------------------------------------------------------------------------------- @@ -291,6 +327,37 @@ class MockServicerImpl_Context(ContextServiceServicer): LOGGER.info('[GetSliceEvents] request={:s}'.format(grpc_message_to_json_string(request))) for message in self.msg_broker.consume({TOPIC_SLICE}): yield SliceEvent(**json.loads(message.content)) + def SelectSlice(self, request : SliceFilter, context : grpc.ServicerContext) -> SliceList: + LOGGER.info('[SelectSlice] request={:s}'.format(grpc_message_to_json_string(request))) + container_entry_uuids : Dict[str, Set[str]] = {} + for slice_id in request.slice_ids.slice_ids: + container_name = 'slice[{:s}]'.format(str(slice_id.context_id.context_uuid.uuid)) + slice_uuid = slice_id.slice_uuid.uuid + container_entry_uuids.setdefault(container_name, set()).add(slice_uuid) + + exclude_endpoint_ids = not request.include_endpoint_ids + exclude_constraints = not request.include_constraints + exclude_service_ids = not request.include_service_ids + exclude_subslice_ids = not request.include_subslice_ids + exclude_config_rules = not request.include_config_rules + + slices = list() + for container_name in sorted(container_entry_uuids.keys()): + entry_uuids = container_entry_uuids[container_name] + for eslice in select_entries(self.database, container_name, entry_uuids): + reply_slice = Slice() + reply_slice.CopyFrom(eslice) + if exclude_endpoint_ids: del reply_slice.service_endpoint_ids[:] # pylint: disable=no-member + if exclude_constraints : del reply_slice.service_constraints[:] # pylint: disable=no-member + if exclude_service_ids : del reply_slice.slice_service_ids[:] # pylint: disable=no-member + if exclude_subslice_ids : del reply_slice.slice_subslice_ids[:] # pylint: disable=no-member + if exclude_config_rules: del reply_slice.slice_config .config_rules[:] # pylint: disable=no-member + slices.append(reply_slice) + + reply = SliceList(slices=slices) + LOGGER.info('[SelectSlice] reply={:s}'.format(grpc_message_to_json_string(reply))) + return reply + # ----- Service ---------------------------------------------------------------------------------------------------- @@ -335,6 +402,32 @@ class MockServicerImpl_Context(ContextServiceServicer): LOGGER.info('[GetServiceEvents] request={:s}'.format(grpc_message_to_json_string(request))) for message in self.msg_broker.consume({TOPIC_SERVICE}): yield ServiceEvent(**json.loads(message.content)) + def SelectService(self, request : ServiceFilter, context : grpc.ServicerContext) -> ServiceList: + LOGGER.info('[SelectService] request={:s}'.format(grpc_message_to_json_string(request))) + container_entry_uuids : Dict[str, Set[str]] = {} + for service_id in request.service_ids.service_ids: + container_name = 'service[{:s}]'.format(str(service_id.context_id.context_uuid.uuid)) + service_uuid = service_id.service_uuid.uuid + container_entry_uuids.setdefault(container_name, set()).add(service_uuid) + + exclude_endpoint_ids = not request.include_endpoint_ids + exclude_constraints = not request.include_constraints + exclude_config_rules = not request.include_config_rules + + services = list() + for container_name in sorted(container_entry_uuids.keys()): + entry_uuids = container_entry_uuids[container_name] + for service in select_entries(self.database, container_name, entry_uuids): + reply_service = Service() + reply_service.CopyFrom(service) + if exclude_endpoint_ids: del reply_service.service_endpoint_ids[:] # pylint: disable=no-member + if exclude_constraints : del reply_service.service_constraints[:] # pylint: disable=no-member + if exclude_config_rules: del reply_service.service_config.config_rules[:] # pylint: disable=no-member + services.append(reply_service) + + reply = ServiceList(services=services) + LOGGER.info('[SelectService] reply={:s}'.format(grpc_message_to_json_string(reply))) + return reply # ----- Connection ------------------------------------------------------------------------------------------------- diff --git a/src/common/tools/object_factory/EndPoint.py b/src/common/tools/object_factory/EndPoint.py index 326c67e1e942e5c4056389a633c70e2830737217..a38ad0d5c59ee75742459729003d43ef01612f53 100644 --- a/src/common/tools/object_factory/EndPoint.py +++ b/src/common/tools/object_factory/EndPoint.py @@ -30,7 +30,7 @@ def json_endpoint_ids( def json_endpoint( device_id : Dict, endpoint_uuid : str, endpoint_type : str, topology_id : Optional[Dict] = None, - kpi_sample_types : List[int] = [] + kpi_sample_types : List[int] = [], location : Optional[Dict] = None ): result = { @@ -38,6 +38,7 @@ def json_endpoint( 'endpoint_type': endpoint_type, } if len(kpi_sample_types) > 0: result['kpi_sample_types'] = copy.deepcopy(kpi_sample_types) + if location: result['endpoint_location'] = copy.deepcopy(location) return result def json_endpoints( diff --git a/src/common/tools/object_factory/Location.py b/src/common/tools/object_factory/Location.py new file mode 100644 index 0000000000000000000000000000000000000000..ac44ef4c64d8466be123c5a722bf7fe69f5e48a8 --- /dev/null +++ b/src/common/tools/object_factory/Location.py @@ -0,0 +1,30 @@ +# 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 +from typing import Dict, Optional + + +def json_gps_position(latitude : float, longitude : float): + return {'latitude': latitude, 'longitude': longitude} + +def json_location(region : Optional[str] = None, gps_position : Optional[Dict] = None): + if not region and not gps_position: + raise Exception('One of "region" or "gps_position" arguments must be filled') + if region: + result = {'region': region} + else: + result = {'gps_position': copy.deepcopy(gps_position)} + + return result diff --git a/src/compute/service/rest_server/nbi_plugins/debug_api/Resources.py b/src/compute/service/rest_server/nbi_plugins/debug_api/Resources.py index 67ef3dfb0ba1519440b0a22f46935165c8388cb8..ffdbba88f077f6490261372f7048b2e2526d8196 100644 --- a/src/compute/service/rest_server/nbi_plugins/debug_api/Resources.py +++ b/src/compute/service/rest_server/nbi_plugins/debug_api/Resources.py @@ -19,7 +19,6 @@ from .Tools import ( format_grpc_to_json, grpc_connection_id, grpc_context_id, grpc_device_id, grpc_link_id, grpc_policy_rule_id, grpc_service_id, grpc_slice_id, grpc_topology_id) - class _Resource(Resource): def __init__(self) -> None: super().__init__() diff --git a/src/context/service/database/Constraint.py b/src/context/service/database/Constraint.py index b33316539e7ab728194bda52e80cbc4896981ca2..2ca49338a6ac0ba2d57ec57dffa26555b9fa6507 100644 --- a/src/context/service/database/Constraint.py +++ b/src/context/service/database/Constraint.py @@ -12,15 +12,16 @@ # See the License for the specific language governing permissions and # limitations under the License. -import datetime, logging +import datetime, json, logging from sqlalchemy import delete #from sqlalchemy.dialects import postgresql from sqlalchemy.dialects.postgresql import insert from sqlalchemy.orm import Session -from typing import Dict, List, Optional +from typing import Dict, List, Optional, Set from common.proto.context_pb2 import Constraint from common.tools.grpc.Tools import grpc_message_to_json_string from .models.ConstraintModel import ConstraintKindEnum, ServiceConstraintModel, SliceConstraintModel +from .models.enums.ConstraintAction import ORM_ConstraintActionEnum, grpc_to_enum__constraint_action from .uuids._Builder import get_uuid_from_string from .uuids.EndPoint import endpoint_get_uuid @@ -37,6 +38,7 @@ def compose_constraints_data( dict_constraint = { 'position' : position, 'kind' : kind, + 'action' : grpc_to_enum__constraint_action(constraint.action), 'data' : grpc_message_to_json_string(getattr(constraint, str_kind, {})), 'created_at': now, 'updated_at': now, @@ -50,7 +52,8 @@ def compose_constraints_data( dict_constraint['slice_uuid'] = slice_uuid parent_kind,parent_uuid = 'slice',slice_uuid else: - MSG = 'Parent for Constraint({:s}) cannot be identified (service_uuid={:s}, slice_uuid={:s})' + MSG = 'Parent for Constraint({:s}) cannot be identified '+\ + '(service_uuid={:s}, slice_uuid={:s})' str_constraint = grpc_message_to_json_string(constraint) raise Exception(MSG.format(str_constraint, str(service_uuid), str(slice_uuid))) @@ -70,7 +73,8 @@ def compose_constraints_data( }: constraint_name = '{:s}:{:s}:'.format(parent_kind, kind.value) else: - MSG = 'Name for Constraint({:s}) cannot be inferred (service_uuid={:s}, slice_uuid={:s})' + MSG = 'Name for Constraint({:s}) cannot be inferred '+\ + '(service_uuid={:s}, slice_uuid={:s})' str_constraint = grpc_message_to_json_string(constraint) raise Exception(MSG.format(str_constraint, str(service_uuid), str(slice_uuid))) @@ -91,27 +95,34 @@ def upsert_constraints( else: MSG = 'DataModel cannot be identified (service_uuid={:s}, slice_uuid={:s})' raise Exception(MSG.format(str(service_uuid), str(slice_uuid))) - + uuids_to_delete : Set[str] = set() uuids_to_upsert : Dict[str, int] = dict() rules_to_upsert : List[Dict] = list() for constraint in constraints: constraint_uuid = constraint['constraint_uuid'] - position = uuids_to_upsert.get(constraint_uuid) - if position is None: - # if not added, add it - rules_to_upsert.append(constraint) - uuids_to_upsert[constraint_uuid] = len(rules_to_upsert) - 1 + constraint_action = constraint['action'] + if is_delete or constraint_action == ORM_ConstraintActionEnum.DELETE: + uuids_to_delete.add(constraint_uuid) + elif constraint_action == ORM_ConstraintActionEnum.SET: + position = uuids_to_upsert.get(constraint_uuid) + if position is None: + # if not added, add it + rules_to_upsert.append(constraint) + uuids_to_upsert[constraint_uuid] = len(rules_to_upsert) - 1 + else: + # if already added, update occurrence + rules_to_upsert[position] = constraint else: - # if already added, update occurrence - rules_to_upsert[position] = constraint + MSG = 'Action for ConstraintRule({:s}) is not supported (service_uuid={:s}, slice_uuid={:s})' + LOGGER.warning(MSG.format(str(constraint), str(service_uuid), str(slice_uuid))) + # raise Exception(MSG.format(str_constraint, str(service_uuid), str(slice_uuid))) - # Delete all constraints not in uuids_to_upsert delete_affected = False - if len(uuids_to_upsert) > 0: + if len(uuids_to_delete) > 0: stmt = delete(klass) if service_uuid is not None: stmt = stmt.where(klass.service_uuid == service_uuid) if slice_uuid is not None: stmt = stmt.where(klass.slice_uuid == slice_uuid ) - stmt = stmt.where(klass.constraint_uuid.not_in(set(uuids_to_upsert.keys()))) + stmt = stmt.where(klass.constraint_uuid.in_(uuids_to_delete) #str_stmt = stmt.compile(dialect=postgresql.dialect(), compile_kwargs={"literal_binds": True}) #LOGGER.warning('delete stmt={:s}'.format(str(str_stmt))) constraint_deletes = session.execute(stmt) @@ -125,6 +136,7 @@ def upsert_constraints( index_elements=[klass.constraint_uuid], set_=dict( position = stmt.excluded.position, + action = stmt.excluded.action, data = stmt.excluded.data, updated_at = stmt.excluded.updated_at, ) diff --git a/src/context/service/database/Device.py b/src/context/service/database/Device.py index 7fc202b9077f2e1212d0c81313fcfbd1c05efb43..8560399cc705729685cbaa7c10399a0ec7589015 100644 --- a/src/context/service/database/Device.py +++ b/src/context/service/database/Device.py @@ -20,7 +20,7 @@ from sqlalchemy_cockroachdb import run_transaction from typing import Dict, List, Optional, Set, Tuple from common.method_wrappers.ServiceExceptions import InvalidArgumentException, NotFoundException from common.proto.context_pb2 import Device, DeviceFilter, DeviceId, TopologyId -#from common.tools.grpc.Tools import grpc_message_to_json_string +from common.tools.grpc.Tools import grpc_message_to_json_string from common.tools.object_factory.Device import json_device_id from context.service.database.uuids.Topology import topology_get_uuid from .models.DeviceModel import DeviceModel @@ -115,14 +115,15 @@ def device_set(db_engine : Engine, request : Device) -> Tuple[Dict, bool]: kpi_sample_types = [grpc_to_enum__kpi_sample_type(kst) for kst in endpoint.kpi_sample_types] endpoints_data.append({ - 'endpoint_uuid' : endpoint_uuid, - 'device_uuid' : endpoint_device_uuid, - 'topology_uuid' : endpoint_topology_uuid, - 'name' : endpoint_name, - 'endpoint_type' : endpoint.endpoint_type, - 'kpi_sample_types': kpi_sample_types, - 'created_at' : now, - 'updated_at' : now, + 'endpoint_uuid' : endpoint_uuid, + 'device_uuid' : endpoint_device_uuid, + 'topology_uuid' : endpoint_topology_uuid, + 'name' : endpoint_name, + 'endpoint_type' : endpoint.endpoint_type, + 'kpi_sample_types' : kpi_sample_types, + 'endpoint_location': grpc_message_to_json_string(endpoint.endpoint_location), + 'created_at' : now, + 'updated_at' : now, }) if endpoint_topology_uuid not in topology_uuids: diff --git a/src/context/service/database/models/ConstraintModel.py b/src/context/service/database/models/ConstraintModel.py index d093e782adde23092d9c9d3949f9153c8ee9d4f3..112a5f35c8d23f4b4b84f5472c14dc3b1bc7fbec 100644 --- a/src/context/service/database/models/ConstraintModel.py +++ b/src/context/service/database/models/ConstraintModel.py @@ -16,6 +16,7 @@ import enum, json from sqlalchemy import CheckConstraint, Column, DateTime, Enum, ForeignKey, Integer, String from sqlalchemy.dialects.postgresql import UUID from typing import Dict +from .enums.ConstraintAction import ORM_ConstraintActionEnum from ._Base import _Base # Enum values should match name of field in Constraint message @@ -39,6 +40,7 @@ class ServiceConstraintModel(_Base): service_uuid = Column(ForeignKey('service.service_uuid', ondelete='CASCADE'), nullable=False) #, index=True position = Column(Integer, nullable=False) kind = Column(Enum(ConstraintKindEnum), nullable=False) + action = Column(Enum(ORM_ConstraintActionEnum), nullable=False) data = Column(String, nullable=False) created_at = Column(DateTime, nullable=False) updated_at = Column(DateTime, nullable=False) @@ -49,7 +51,10 @@ class ServiceConstraintModel(_Base): ) def dump(self) -> Dict: - return {self.kind.value: json.loads(self.data)} + return { + 'action': self.action.value, + self.kind.value: json.loads(self.data), + } class SliceConstraintModel(_Base): __tablename__ = 'slice_constraint' @@ -58,6 +63,7 @@ class SliceConstraintModel(_Base): slice_uuid = Column(ForeignKey('slice.slice_uuid', ondelete='CASCADE'), nullable=False) #, index=True position = Column(Integer, nullable=False) kind = Column(Enum(ConstraintKindEnum), nullable=False) + action = Column(Enum(ORM_ConstraintActionEnum), nullable=False) data = Column(String, nullable=False) created_at = Column(DateTime, nullable=False) updated_at = Column(DateTime, nullable=False) @@ -68,4 +74,7 @@ class SliceConstraintModel(_Base): ) def dump(self) -> Dict: - return {self.kind.value: json.loads(self.data)} + return { + 'action': self.action.value, + self.kind.value: json.loads(self.data), + } diff --git a/src/context/service/database/models/EndPointModel.py b/src/context/service/database/models/EndPointModel.py index a079f9900e39fdf3a4329e604f4e596e7f5d1f89..9bb2adb2d4209bde8cee4299d10521887f371428 100644 --- a/src/context/service/database/models/EndPointModel.py +++ b/src/context/service/database/models/EndPointModel.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +import json from sqlalchemy import Column, DateTime, Enum, ForeignKey, String from sqlalchemy.dialects.postgresql import ARRAY, UUID from sqlalchemy.orm import relationship @@ -22,14 +23,15 @@ from ._Base import _Base class EndPointModel(_Base): __tablename__ = 'endpoint' - endpoint_uuid = Column(UUID(as_uuid=False), primary_key=True) - device_uuid = Column(ForeignKey('device.device_uuid', ondelete='CASCADE' ), nullable=False, index=True) - topology_uuid = Column(ForeignKey('topology.topology_uuid', ondelete='RESTRICT'), nullable=False, index=True) - name = Column(String, nullable=False) - endpoint_type = Column(String, nullable=False) - kpi_sample_types = Column(ARRAY(Enum(ORM_KpiSampleTypeEnum), dimensions=1)) - created_at = Column(DateTime, nullable=False) - updated_at = Column(DateTime, nullable=False) + endpoint_uuid = Column(UUID(as_uuid=False), primary_key=True) + device_uuid = Column(ForeignKey('device.device_uuid', ondelete='CASCADE' ), nullable=False, index=True) + topology_uuid = Column(ForeignKey('topology.topology_uuid', ondelete='RESTRICT'), nullable=False, index=True) + name = Column(String, nullable=False) + endpoint_type = Column(String, nullable=False) + kpi_sample_types = Column(ARRAY(Enum(ORM_KpiSampleTypeEnum), dimensions=1)) + created_at = Column(DateTime, nullable=False) + updated_at = Column(DateTime, nullable=False) + endpoint_location = Column(String, nullable=True) device = relationship('DeviceModel', back_populates='endpoints') # lazy='selectin' topology = relationship('TopologyModel', lazy='selectin') @@ -46,10 +48,11 @@ class EndPointModel(_Base): def dump(self) -> Dict: return { - 'endpoint_id' : self.dump_id(), - 'name' : self.name, - 'endpoint_type' : self.endpoint_type, - 'kpi_sample_types': [kst.value for kst in self.kpi_sample_types], + 'endpoint_id' : self.dump_id(), + 'name' : self.name, + 'endpoint_type' : self.endpoint_type, + 'kpi_sample_types' : [kst.value for kst in self.kpi_sample_types], + 'endpoint_location': json.loads(self.endpoint_location) } def dump_name(self) -> Dict: diff --git a/src/context/service/database/models/enums/ConstraintAction.py b/src/context/service/database/models/enums/ConstraintAction.py new file mode 100644 index 0000000000000000000000000000000000000000..65533b6f579ffe153b046dfcb39d37758f4c0577 --- /dev/null +++ b/src/context/service/database/models/enums/ConstraintAction.py @@ -0,0 +1,25 @@ +# 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 enum, functools +from common.proto.context_pb2 import ConstraintActionEnum +from ._GrpcToEnum import grpc_to_enum + +class ORM_ConstraintActionEnum(enum.Enum): + UNDEFINED = ConstraintActionEnum.CONSTRAINTACTION_UNDEFINED + SET = ConstraintActionEnum.CONSTRAINTACTION_SET + DELETE = ConstraintActionEnum.CONSTRAINTACTION_DELETE + +grpc_to_enum__constraint_action = functools.partial( + grpc_to_enum, ConstraintActionEnum, ORM_ConstraintActionEnum) diff --git a/src/device/service/Tools.py b/src/device/service/Tools.py index 8717254cb59ad1b83a6e65ca3c1ba68757663674..c242ab1f64995a0e5d7bc21ae9300c70cb888ff8 100644 --- a/src/device/service/Tools.py +++ b/src/device/service/Tools.py @@ -16,12 +16,11 @@ import json, logging from typing import Any, Dict, List, Optional, Tuple, Union from common.Constants import DEFAULT_CONTEXT_NAME, DEFAULT_TOPOLOGY_NAME from common.method_wrappers.ServiceExceptions import InvalidArgumentException -from common.proto.context_pb2 import ConfigActionEnum, Device, DeviceConfig, Link +from common.proto.context_pb2 import ConfigActionEnum, Device, DeviceConfig, Link, Location from common.proto.device_pb2 import MonitoringSettings from common.proto.kpi_sample_types_pb2 import KpiSampleType from common.tools.grpc.ConfigRules import update_config_rule_custom from common.tools.grpc.Tools import grpc_message_to_json -from context.client.ContextClient import ContextClient from .driver_api._Driver import _Driver, RESOURCE_ENDPOINTS from .monitoring.MonitoringLoops import MonitoringLoops from .ErrorMessages import ( @@ -202,6 +201,10 @@ def populate_endpoints( device_endpoint.kpi_sample_types.append(kpi_sample_type) monitoring_loops.add_resource_key(device_uuid, endpoint_uuid, kpi_sample_type, monitor_resource_key) + location = resource_value.get('location', None) + if location is not None: + device_endpoint.endpoint_location.MergeFrom(Location(**location)) + elif resource_key.startswith('/links/link'): # create sub-link _sub_link_uuid = resource_value['uuid'] diff --git a/src/device/service/drivers/emulated/Tools.py b/src/device/service/drivers/emulated/Tools.py index 0ac92bf56d5538a5ed4d3e7c53bc480d5ecd40bd..15fa342388159a19bf071985ab1a7fc74d8b28a2 100644 --- a/src/device/service/drivers/emulated/Tools.py +++ b/src/device/service/drivers/emulated/Tools.py @@ -79,7 +79,10 @@ def compose_resource_endpoint(endpoint_data : Dict[str, Any]) -> Optional[Tuple[ if len(sample_types) > 0: endpoint_resource_value['sample_types'] = sample_types - + + if 'location' in endpoint_data: + endpoint_resource_value['location'] = endpoint_data['location'] + return endpoint_resource_key, endpoint_resource_value except: # pylint: disable=bare-except LOGGER.exception('Problem composing endpoint({:s})'.format(str(endpoint_data))) diff --git a/src/service/requirements.in b/src/service/requirements.in index 83b6342c0c0ed5e969ba03e8af5ac502b1f525c9..48fd76485d6bbaf53c3867147882614fc0cf1b04 100644 --- a/src/service/requirements.in +++ b/src/service/requirements.in @@ -14,6 +14,7 @@ anytree==2.8.0 +geopy==2.3.0 networkx==2.6.3 pydot==1.4.2 redis==4.1.2 diff --git a/src/service/service/ServiceServiceServicerImpl.py b/src/service/service/ServiceServiceServicerImpl.py index 6d23fd4cee53d1639c9eefbd943d45dab497b253..11d191d4b08d642c228bbfc5ce33c45bd587021a 100644 --- a/src/service/service/ServiceServiceServicerImpl.py +++ b/src/service/service/ServiceServiceServicerImpl.py @@ -12,13 +12,13 @@ # See the License for the specific language governing permissions and # limitations under the License. -import grpc, json, logging, random, uuid +import copy, grpc, json, logging, random, uuid from typing import Optional from common.method_wrappers.Decorator import MetricsPool, safe_and_metered_rpc_method from common.method_wrappers.ServiceExceptions import ( AlreadyExistsException, InvalidArgumentException, NotFoundException, NotImplementedException, OperationFailedException) -from common.proto.context_pb2 import Connection, Empty, Service, ServiceId, ServiceStatusEnum, ServiceTypeEnum +from common.proto.context_pb2 import Connection, Empty, Service, ServiceId, ServiceStatusEnum, ServiceTypeEnum, ConstraintActionEnum from common.proto.pathcomp_pb2 import PathCompRequest from common.proto.service_pb2_grpc import ServiceServiceServicer from common.tools.context_queries.Service import get_service_by_id @@ -28,6 +28,7 @@ from pathcomp.frontend.client.PathCompClient import PathCompClient from service.service.tools.ConnectionToString import connection_to_string from .service_handler_api.ServiceHandlerFactory import ServiceHandlerFactory from .task_scheduler.TaskScheduler import TasksScheduler +from .tools.GeodesicDistance import gps_distance LOGGER = logging.getLogger(__name__) @@ -96,6 +97,28 @@ class ServiceServiceServicerImpl(ServiceServiceServicer): include_config_rules=False, include_constraints=False, include_endpoint_ids=False) service = Service() service.CopyFrom(request if _service is None else _service) + + for constraint in request.service_constraints: + if constraint.action == ConstraintActionEnum.CONSTRAINTACTION_SET: + if constraint.WhichOneof('constraint') == 'endpoint_location' and not constraint.endpoint_location.HasField('endpoint_id'): + device_list = context_client.ListDevices(Empty()) + service_location = constraint.endpoint_location.location + distances = {} + for device in device_list.devices: + for endpoint in device.device_endpoints: + if not endpoint.endpoint_location.HasField('gps_position'): continue + + distance = gps_distance(service_location.gps_position, endpoint.endpoint_location.gps_position) + distances[distance] = endpoint.endpoint_id + + closer_endpoint_id = distances[min(distances)] + constraint.endpoint_location.endpoint_id.CopyFrom(closer_endpoint_id) + + if closer_endpoint_id not in [endpoint.endpoint_id.endpoint_uuid for endpoint in service.service_endpoint_ids]: + service.service_endpoint_ids.append(closer_endpoint_id) + + + if service.service_type == ServiceTypeEnum.SERVICETYPE_UNKNOWN: # pylint: disable=no-member service.service_type = request.service_type # pylint: disable=no-member service.service_status.service_status = ServiceStatusEnum.SERVICESTATUS_PLANNED # pylint: disable=no-member diff --git a/src/service/service/tools/GeodesicDistance.py b/src/service/service/tools/GeodesicDistance.py new file mode 100644 index 0000000000000000000000000000000000000000..b66d336f0f617aa834905785e2e49f95073a2df9 --- /dev/null +++ b/src/service/service/tools/GeodesicDistance.py @@ -0,0 +1,18 @@ +# 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 geopy.distance import geodesic + +def gps_distance(gps1, gps2): + return geodesic((gps1.latitude, gps1.longitude), (gps2.latitude, gps2.longitude)).km diff --git a/src/service/tests/PrepareTestScenario.py b/src/service/tests/PrepareTestScenario.py index e4609ec416803312926422aca16cb02a6785a789..a5244f5a7c3fe35089c52c077db5a57b1e69ba5b 100644 --- a/src/service/tests/PrepareTestScenario.py +++ b/src/service/tests/PrepareTestScenario.py @@ -26,7 +26,7 @@ from service.tests.MockService_Dependencies import MockService_Dependencies LOCAL_HOST = '127.0.0.1' MOCKSERVICE_PORT = 10000 -SERVICE_SERVICE_PORT = MOCKSERVICE_PORT + get_service_port_grpc(ServiceNameEnum.SERVICE) # avoid privileged ports +SERVICE_SERVICE_PORT = MOCKSERVICE_PORT + int(get_service_port_grpc(ServiceNameEnum.SERVICE)) # avoid privileged ports os.environ[get_env_var_name(ServiceNameEnum.SERVICE, ENVVAR_SUFIX_SERVICE_HOST )] = str(LOCAL_HOST) os.environ[get_env_var_name(ServiceNameEnum.SERVICE, ENVVAR_SUFIX_SERVICE_PORT_GRPC)] = str(SERVICE_SERVICE_PORT) diff --git a/src/service/tests/ServiceHandler_L3NM_EMU.py b/src/service/tests/ServiceHandler_L3NM_EMU.py index 3df27b439626dad652e443cca4195ce36f4ac86f..2618e204c00a1bb956812daeb6f831275cac7b5a 100644 --- a/src/service/tests/ServiceHandler_L3NM_EMU.py +++ b/src/service/tests/ServiceHandler_L3NM_EMU.py @@ -13,8 +13,10 @@ # limitations under the License. from typing import Dict, List, Tuple + +from common.tools.object_factory.Location import json_location, json_gps_position from common.tools.object_factory.ConfigRule import json_config_rule_set -from common.tools.object_factory.Constraint import json_constraint_custom +from common.tools.object_factory.Constraint import json_constraint_custom, json_constraint_endpoint_location_gps from common.tools.object_factory.Device import ( json_device_emulated_packet_router_disabled, json_device_emulated_tapi_disabled, json_device_id) from common.tools.object_factory.EndPoint import json_endpoint, json_endpoint_id @@ -24,21 +26,37 @@ from .CommonObjects import CONTEXT, CONTEXT_ID, PACKET_PORT_SAMPLE_TYPES, TOPOLO SERVICE_HANDLER_NAME = 'l3nm_emulated' -def json_endpoint_ids(device_id : Dict, endpoint_descriptors : List[Tuple[str, str]]): +def json_endpoint_ids(device_id : Dict, endpoint_descriptors : List[Tuple[str, str, str]]): return [ json_endpoint_id(device_id, ep_uuid) - for ep_uuid, _ in endpoint_descriptors + for ep_uuid, _, _ in endpoint_descriptors ] -def json_endpoints(device_id : Dict, endpoint_descriptors : List[Tuple[str, str]]): +def json_endpoints(device_id : Dict, endpoint_descriptors : List[Tuple[str, str, str]]): return [ - json_endpoint(device_id, ep_uuid, ep_type, kpi_sample_types=PACKET_PORT_SAMPLE_TYPES) - for ep_uuid, ep_type in endpoint_descriptors + json_endpoint(device_id, ep_uuid, ep_type, kpi_sample_types=PACKET_PORT_SAMPLE_TYPES, location=ep_location) + for ep_uuid, ep_type, ep_location in endpoint_descriptors ] + +BARCELONA_GPS = (41.386726, 2.170107) +MALAGA_GPS = (36.721162, -4.418339) +ZARAGOZA_GPS = (41.655552, -0.876442) +MADRID_GPS = (40.416741, -3.703285) +TOLEDO_GPS = (39.862947, -4.027485) +ANDORRA_GPS = (42.506017, 1.525923) +SANTIAGO_GPS = (42.876254, -8.543588) +GRANADA_GPS = (37.178106, -3.599816) +PONFERRADA_GPS = (42.550116, -6.597930) +ALBACETE_GPS = (38.998249, -1.858145) + + # ----- Devices -------------------------------------------------------------------------------------------------------- DEVICE_R1_UUID = 'R1' -DEVICE_R1_ENDPOINT_DEFS = [('EP1', 'optical'), ('EP100', 'copper')] +DEVICE_R1_ENDPOINT_DEFS = [ + ('EP1', 'optical', json_location(gps_position=json_gps_position(*BARCELONA_GPS))), + ('EP100', 'copper', json_location(gps_position=json_gps_position(*BARCELONA_GPS))) +] DEVICE_R1_ID = json_device_id(DEVICE_R1_UUID) DEVICE_R1_ENDPOINTS = json_endpoints(DEVICE_R1_ID, DEVICE_R1_ENDPOINT_DEFS) DEVICE_R1_ENDPOINT_IDS = json_endpoint_ids(DEVICE_R1_ID, DEVICE_R1_ENDPOINT_DEFS) @@ -47,7 +65,10 @@ ENDPOINT_ID_R1_EP1 = DEVICE_R1_ENDPOINT_IDS[0] ENDPOINT_ID_R1_EP100 = DEVICE_R1_ENDPOINT_IDS[1] DEVICE_R2_UUID = 'R2' -DEVICE_R2_ENDPOINT_DEFS = [('EP1', 'optical'), ('EP100', 'copper')] +DEVICE_R2_ENDPOINT_DEFS = [ + ('EP1', 'optical', json_location(gps_position=json_gps_position(*MADRID_GPS))), + ('EP100', 'copper', json_location(gps_position=json_gps_position(*MADRID_GPS))) +] DEVICE_R2_ID = json_device_id(DEVICE_R2_UUID) DEVICE_R2_ENDPOINTS = json_endpoints(DEVICE_R2_ID, DEVICE_R2_ENDPOINT_DEFS) DEVICE_R2_ENDPOINT_IDS = json_endpoint_ids(DEVICE_R2_ID, DEVICE_R2_ENDPOINT_DEFS) @@ -56,7 +77,10 @@ ENDPOINT_ID_R2_EP1 = DEVICE_R2_ENDPOINT_IDS[0] ENDPOINT_ID_R2_EP100 = DEVICE_R2_ENDPOINT_IDS[1] DEVICE_R3_UUID = 'R3' -DEVICE_R3_ENDPOINT_DEFS = [('EP1', 'optical'), ('EP100', 'copper')] +DEVICE_R3_ENDPOINT_DEFS = [ + ('EP1', 'optical', json_location(gps_position=json_gps_position(*MALAGA_GPS))), + ('EP100', 'copper', json_location(gps_position=json_gps_position(*MALAGA_GPS))) +] DEVICE_R3_ID = json_device_id(DEVICE_R3_UUID) DEVICE_R3_ENDPOINTS = json_endpoints(DEVICE_R3_ID, DEVICE_R3_ENDPOINT_DEFS) DEVICE_R3_ENDPOINT_IDS = json_endpoint_ids(DEVICE_R3_ID, DEVICE_R3_ENDPOINT_DEFS) @@ -65,7 +89,11 @@ ENDPOINT_ID_R3_EP1 = DEVICE_R3_ENDPOINT_IDS[0] ENDPOINT_ID_R3_EP100 = DEVICE_R3_ENDPOINT_IDS[1] DEVICE_O1_UUID = 'O1' -DEVICE_O1_ENDPOINT_DEFS = [('EP1', 'optical'), ('EP2', 'optical'), ('EP3', 'optical')] +DEVICE_O1_ENDPOINT_DEFS = [ + ('EP1', 'optical', json_location(gps_position=json_gps_position(*PONFERRADA_GPS))), + ('EP2', 'optical', json_location(gps_position=json_gps_position(*PONFERRADA_GPS))), + ('EP3', 'optical', json_location(gps_position=json_gps_position(*PONFERRADA_GPS))) +] DEVICE_O1_ID = json_device_id(DEVICE_O1_UUID) DEVICE_O1_ENDPOINTS = json_endpoints(DEVICE_O1_ID, DEVICE_O1_ENDPOINT_DEFS) DEVICE_O1_ENDPOINT_IDS = json_endpoint_ids(DEVICE_O1_ID, DEVICE_O1_ENDPOINT_DEFS) @@ -104,6 +132,18 @@ SERVICE_R1_R3_CONSTRAINTS = [ json_constraint_custom('latency_ms', 15.2), json_constraint_custom('jitter_us', 1.2), ] + + +SERVICE_R1_R3_CONSTRAINTS_LOCATION = [ + json_constraint_endpoint_location_gps(None, ZARAGOZA_GPS[0], ZARAGOZA_GPS[1]), + json_constraint_endpoint_location_gps(None, TOLEDO_GPS[0], TOLEDO_GPS[1]), +] +SERVICE_R1_R3_CONSTRAINTS_LOCATION_NEW = [ + json_constraint_endpoint_location_gps(None, SANTIAGO_GPS[0], SANTIAGO_GPS[1]), + json_constraint_endpoint_location_gps(None, GRANADA_GPS[0], GRANADA_GPS[1]), +] + + SERVICE_R1_R3_CONFIG_RULES = [ json_config_rule_set( '/settings', @@ -123,14 +163,15 @@ SERVICE_R1_R3_DESCRIPTOR = json_service_l3nm_planned(SERVICE_R1_R3_UUID) # ----- Test Descriptor ------------------------------------------------------------------------------------------------ TEST_SERVICE_HANDLER = (SERVICE_HANDLER_NAME, { - 'contexts' : [CONTEXT], - 'topologies' : [TOPOLOGY], - 'devices' : [DEVICE_R1, DEVICE_R2, DEVICE_R3, DEVICE_O1], - 'links' : [LINK_R1_O1, LINK_R2_O1, LINK_R3_O1], - - 'service_id' : SERVICE_R1_R3_ID, - 'service_descriptor' : SERVICE_R1_R3_DESCRIPTOR, - 'service_endpoint_ids' : SERVICE_R1_R3_ENDPOINT_IDS, - 'service_config_rules' : SERVICE_R1_R3_CONFIG_RULES, - 'service_constraints' : SERVICE_R1_R3_CONSTRAINTS, + 'contexts' : [CONTEXT], + 'topologies' : [TOPOLOGY], + 'devices' : [DEVICE_R1, DEVICE_R2, DEVICE_R3, DEVICE_O1], + 'links' : [LINK_R1_O1, LINK_R2_O1, LINK_R3_O1], + 'service_id' : SERVICE_R1_R3_ID, + 'service_descriptor' : SERVICE_R1_R3_DESCRIPTOR, + 'service_endpoint_ids' : SERVICE_R1_R3_ENDPOINT_IDS, + 'service_config_rules' : SERVICE_R1_R3_CONFIG_RULES, + 'service_constraints' : SERVICE_R1_R3_CONSTRAINTS, + 'service_constraints_location' : SERVICE_R1_R3_CONSTRAINTS_LOCATION, + 'service_constraints_location_new' : SERVICE_R1_R3_CONSTRAINTS_LOCATION_NEW, }) diff --git a/src/service/tests/test_unitary.py b/src/service/tests/test_unitary.py index f99f9b191083db017486d03d0bc93b0d9152b35f..aa44714af081307377fe52106cb715f5e0774c73 100644 --- a/src/service/tests/test_unitary.py +++ b/src/service/tests/test_unitary.py @@ -38,6 +38,7 @@ class TestServiceHandlers: def test_prepare_environment( self, service_id, service_descriptor, service_endpoint_ids, service_config_rules, service_constraints, + service_constraints_location, service_constraints_location_new, contexts, topologies, devices, links, context_client : ContextClient, # pylint: disable=redefined-outer-name device_client : DeviceClient, # pylint: disable=redefined-outer-name @@ -51,6 +52,7 @@ class TestServiceHandlers: def test_service_create_error_cases( self, service_id, service_descriptor, service_endpoint_ids, service_config_rules, service_constraints, + service_constraints_location, service_constraints_location_new, contexts, topologies, devices, links, context_client : ContextClient, # pylint: disable=redefined-outer-name device_client : DeviceClient, # pylint: disable=redefined-outer-name @@ -92,6 +94,7 @@ class TestServiceHandlers: def test_service_create_correct( self, service_id, service_descriptor, service_endpoint_ids, service_config_rules, service_constraints, + service_constraints_location, service_constraints_location_new, contexts, topologies, devices, links, context_client : ContextClient, # pylint: disable=redefined-outer-name device_client : DeviceClient, # pylint: disable=redefined-outer-name @@ -102,6 +105,7 @@ class TestServiceHandlers: def test_service_get_created( self, service_id, service_descriptor, service_endpoint_ids, service_config_rules, service_constraints, + service_constraints_location, service_constraints_location_new, contexts, topologies, devices, links, context_client : ContextClient, # pylint: disable=redefined-outer-name device_client : DeviceClient, # pylint: disable=redefined-outer-name @@ -113,6 +117,7 @@ class TestServiceHandlers: def test_service_update_configure( self, service_id, service_descriptor, service_endpoint_ids, service_config_rules, service_constraints, + service_constraints_location, service_constraints_location_new, contexts, topologies, devices, links, context_client : ContextClient, # pylint: disable=redefined-outer-name device_client : DeviceClient, # pylint: disable=redefined-outer-name @@ -123,7 +128,6 @@ class TestServiceHandlers: service_with_settings['service_config']['config_rules'].extend(service_config_rules) service_with_settings['service_constraints'].extend(service_constraints) service_client.UpdateService(Service(**service_with_settings)) - for endpoint_id in service_endpoint_ids: device_id = endpoint_id['device_id'] device_data = context_client.GetDevice(DeviceId(**device_id)) @@ -131,9 +135,9 @@ class TestServiceHandlers: LOGGER.info('device_data[{:s}][#{:d}] => {:s}'.format( str(device_id), i, grpc_message_to_json_string(config_rule))) - def test_service_update_deconfigure( self, service_id, service_descriptor, service_endpoint_ids, service_config_rules, service_constraints, + service_constraints_location, service_constraints_location_new, contexts, topologies, devices, links, context_client : ContextClient, # pylint: disable=redefined-outer-name device_client : DeviceClient, # pylint: disable=redefined-outer-name @@ -153,6 +157,7 @@ class TestServiceHandlers: def test_service_get_updated( self, service_id, service_descriptor, service_endpoint_ids, service_config_rules, service_constraints, + service_constraints_location, service_constraints_location_new, contexts, topologies, devices, links, context_client : ContextClient, # pylint: disable=redefined-outer-name device_client : DeviceClient, # pylint: disable=redefined-outer-name @@ -162,18 +167,84 @@ class TestServiceHandlers: LOGGER.info('service_data = {:s}'.format(grpc_message_to_json_string(service_data))) - def test_service_delete( + def test_service_update_configure_loc( self, service_id, service_descriptor, service_endpoint_ids, service_config_rules, service_constraints, + service_constraints_location, service_constraints_location_new, contexts, topologies, devices, links, context_client : ContextClient, # pylint: disable=redefined-outer-name device_client : DeviceClient, # pylint: disable=redefined-outer-name service_client : ServiceClient): # pylint: disable=redefined-outer-name - service_client.DeleteService(ServiceId(**service_id)) + service_with_settings = copy.deepcopy(service_descriptor) + service_with_settings['service_config']['config_rules'].extend(service_config_rules) + service_with_settings['service_constraints'].extend(service_constraints_location) + service_client.UpdateService(Service(**service_with_settings)) + for endpoint_id in service_endpoint_ids: + device_id = endpoint_id['device_id'] + device_data = context_client.GetDevice(DeviceId(**device_id)) + for i,config_rule in enumerate(device_data.device_config.config_rules): + LOGGER.info('device_data[{:s}][#{:d}] => {:s}'.format( + str(device_id), i, grpc_message_to_json_string(config_rule))) + + + def test_service_get_updated_1( + self, service_id, service_descriptor, service_endpoint_ids, service_config_rules, service_constraints, + service_constraints_location, service_constraints_location_new, + contexts, topologies, devices, links, + context_client : ContextClient, # pylint: disable=redefined-outer-name + device_client : DeviceClient, # pylint: disable=redefined-outer-name + service_client : ServiceClient): # pylint: disable=redefined-outer-name + + service_data = context_client.GetService(ServiceId(**service_id)) + LOGGER.info('service_data = {:s}'.format(grpc_message_to_json_string(service_data))) + + + def test_service_update_configure_loc_new( + self, service_id, service_descriptor, service_endpoint_ids, service_config_rules, service_constraints, + service_constraints_location, service_constraints_location_new, + contexts, topologies, devices, links, + context_client : ContextClient, # pylint: disable=redefined-outer-name + device_client : DeviceClient, # pylint: disable=redefined-outer-name + service_client : ServiceClient): # pylint: disable=redefined-outer-name + + service_with_settings = copy.deepcopy(service_descriptor) + service_with_settings['service_config']['config_rules'].extend(service_config_rules) + service_with_settings['service_constraints'].extend(service_constraints_location_new) + service_client.UpdateService(Service(**service_with_settings)) + + for endpoint_id in service_endpoint_ids: + device_id = endpoint_id['device_id'] + device_data = context_client.GetDevice(DeviceId(**device_id)) + for i,config_rule in enumerate(device_data.device_config.config_rules): + LOGGER.info('device_data[{:s}][#{:d}] => {:s}'.format( + str(device_id), i, grpc_message_to_json_string(config_rule))) + + + def test_service_get_updated_2( + self, service_id, service_descriptor, service_endpoint_ids, service_config_rules, service_constraints, + service_constraints_location, service_constraints_location_new, + contexts, topologies, devices, links, + context_client : ContextClient, # pylint: disable=redefined-outer-name + device_client : DeviceClient, # pylint: disable=redefined-outer-name + service_client : ServiceClient): # pylint: disable=redefined-outer-name + + service_data = context_client.GetService(ServiceId(**service_id)) + LOGGER.info('service_data = {:s}'.format(grpc_message_to_json_string(service_data))) + + def test_service_delete_loc( + self, service_id, service_descriptor, service_endpoint_ids, service_config_rules, service_constraints, + service_constraints_location, service_constraints_location_new, + contexts, topologies, devices, links, + context_client : ContextClient, # pylint: disable=redefined-outer-name + device_client : DeviceClient, # pylint: disable=redefined-outer-name + service_client : ServiceClient): # pylint: disable=redefined-outer-name + + service_client.DeleteService(ServiceId(**service_id)) def test_cleanup_environment( self, service_id, service_descriptor, service_endpoint_ids, service_config_rules, service_constraints, + service_constraints_location, service_constraints_location_new, contexts, topologies, devices, links, context_client : ContextClient, # pylint: disable=redefined-outer-name device_client : DeviceClient, # pylint: disable=redefined-outer-name diff --git a/src/webui/service/templates/device/detail.html b/src/webui/service/templates/device/detail.html index 4d33578e2532c26b4062565bd2cbb52106773a1a..c35ae163d3f8344f1ebb49241cc15a4afa3401d5 100644 --- a/src/webui/service/templates/device/detail.html +++ b/src/webui/service/templates/device/detail.html @@ -63,6 +63,7 @@ Endpoint UUID Name Type + Location @@ -77,6 +78,9 @@ {{ endpoint.endpoint_type }} + + {{ endpoint.endpoint_location }} + {% endfor %}