diff --git a/common_requirements.in b/common_requirements.in
index 39982ebe75dedf8bfaceebe1bfcf986a815ea7ff..37b70c993e913602f9d5e509d0c887802c5d0b1e 100644
--- a/common_requirements.in
+++ b/common_requirements.in
@@ -16,7 +16,7 @@ coverage==6.3
grpcio==1.47.*
grpcio-health-checking==1.47.*
grpcio-tools==1.47.*
-grpclib[protobuf]
+grpclib==0.4.4
prettytable==3.5.0
prometheus-client==0.13.0
protobuf==3.20.*
diff --git a/proto/context.proto b/proto/context.proto
index 3b25e6361766ee4c2b52e15aab215409f40cbb56..3104f1b545c02bab71c8638ebba03efdcbfe71ff 100644
--- a/proto/context.proto
+++ b/proto/context.proto
@@ -175,6 +175,7 @@ message Device {
repeated DeviceDriverEnum device_drivers = 6;
repeated EndPoint device_endpoints = 7;
repeated Component component = 8; // Used for inventory
+ DeviceId controller_id = 9; // Identifier of node controlling the actual device
}
message Component {
@@ -276,9 +277,10 @@ enum ServiceTypeEnum {
enum ServiceStatusEnum {
SERVICESTATUS_UNDEFINED = 0;
SERVICESTATUS_PLANNED = 1;
- SERVICESTATUS_ACTIVE = 2;
- SERVICESTATUS_PENDING_REMOVAL = 3;
- SERVICESTATUS_SLA_VIOLATED = 4;
+ SERVICESTATUS_ACTIVE = 2;
+ SERVICESTATUS_UPDATING = 3;
+ SERVICESTATUS_PENDING_REMOVAL = 4;
+ SERVICESTATUS_SLA_VIOLATED = 5;
}
message ServiceStatus {
@@ -554,6 +556,13 @@ message Constraint_SLA_Isolation_level {
repeated IsolationLevelEnum isolation_level = 1;
}
+message Constraint_Exclusions {
+ bool is_permanent = 1;
+ repeated DeviceId device_ids = 2;
+ repeated EndPointId endpoint_ids = 3;
+ repeated LinkId link_ids = 4;
+}
+
message Constraint {
oneof constraint {
Constraint_Custom custom = 1;
@@ -564,6 +573,7 @@ message Constraint {
Constraint_SLA_Latency sla_latency = 6;
Constraint_SLA_Availability sla_availability = 7;
Constraint_SLA_Isolation_level sla_isolation = 8;
+ Constraint_Exclusions exclusions = 9;
}
}
diff --git a/proto/service.proto b/proto/service.proto
index 21e5699413cc4842962af6ee9c204b383fc61ec0..658859e3c5aac58e792d508a89b467e937198c5b 100644
--- a/proto/service.proto
+++ b/proto/service.proto
@@ -18,7 +18,8 @@ package service;
import "context.proto";
service ServiceService {
- rpc CreateService(context.Service ) returns (context.ServiceId) {}
- rpc UpdateService(context.Service ) returns (context.ServiceId) {}
- rpc DeleteService(context.ServiceId) returns (context.Empty ) {}
+ rpc CreateService (context.Service ) returns (context.ServiceId) {}
+ rpc UpdateService (context.Service ) returns (context.ServiceId) {}
+ rpc DeleteService (context.ServiceId) returns (context.Empty ) {}
+ rpc RecomputeConnections(context.Service ) returns (context.Empty ) {}
}
diff --git a/src/common/type_checkers/Assertions.py b/src/common/type_checkers/Assertions.py
index ba82e535ec958104bd14abf625eb6cd38c2a08ee..d5476a9534ca6e2d74ba16d3af71ed367bc5ab51 100644
--- a/src/common/type_checkers/Assertions.py
+++ b/src/common/type_checkers/Assertions.py
@@ -69,7 +69,9 @@ def validate_service_state_enum(message):
'SERVICESTATUS_UNDEFINED',
'SERVICESTATUS_PLANNED',
'SERVICESTATUS_ACTIVE',
+ 'SERVICESTATUS_UPDATING',
'SERVICESTATUS_PENDING_REMOVAL',
+ 'SERVICESTATUS_SLA_VIOLATED',
]
diff --git a/src/context/service/database/Constraint.py b/src/context/service/database/Constraint.py
index 592d7f4c545a222092ca95924afafa69d2798d7c..b33316539e7ab728194bda52e80cbc4896981ca2 100644
--- a/src/context/service/database/Constraint.py
+++ b/src/context/service/database/Constraint.py
@@ -66,7 +66,7 @@ def compose_constraints_data(
constraint_name = '{:s}:{:s}:{:s}'.format(parent_kind, kind.value, endpoint_uuid)
elif kind in {
ConstraintKindEnum.SCHEDULE, ConstraintKindEnum.SLA_CAPACITY, ConstraintKindEnum.SLA_LATENCY,
- ConstraintKindEnum.SLA_AVAILABILITY, ConstraintKindEnum.SLA_ISOLATION
+ ConstraintKindEnum.SLA_AVAILABILITY, ConstraintKindEnum.SLA_ISOLATION, ConstraintKindEnum.EXCLUSIONS
}:
constraint_name = '{:s}:{:s}:'.format(parent_kind, kind.value)
else:
diff --git a/src/context/service/database/Device.py b/src/context/service/database/Device.py
index 3e106bc158ab804c7eada7284e9d1b883eb66264..7fc202b9077f2e1212d0c81313fcfbd1c05efb43 100644
--- a/src/context/service/database/Device.py
+++ b/src/context/service/database/Device.py
@@ -74,6 +74,11 @@ def device_set(db_engine : Engine, request : Device) -> Tuple[Dict, bool]:
device_name = raw_device_uuid if len(raw_device_name) == 0 else raw_device_name
device_uuid = device_get_uuid(request.device_id, device_name=device_name, allow_random=True)
+ if len(request.controller_id.device_uuid.uuid) > 0:
+ controller_uuid = device_get_uuid(request.controller_id, allow_random=False)
+ else:
+ controller_uuid = None
+
device_type = request.device_type
oper_status = grpc_to_enum__device_operational_status(request.device_operational_status)
device_drivers = [grpc_to_enum__device_driver(d) for d in request.device_drivers]
@@ -139,6 +144,9 @@ def device_set(db_engine : Engine, request : Device) -> Tuple[Dict, bool]:
'updated_at' : now,
}]
+ if controller_uuid is not None:
+ device_data[0]['controller_uuid'] = controller_uuid
+
def callback(session : Session) -> bool:
stmt = insert(DeviceModel).values(device_data)
stmt = stmt.on_conflict_do_update(
diff --git a/src/context/service/database/models/ConstraintModel.py b/src/context/service/database/models/ConstraintModel.py
index cbbe0b5d7280a6f14d645b66abd4df444abb41aa..d093e782adde23092d9c9d3949f9153c8ee9d4f3 100644
--- a/src/context/service/database/models/ConstraintModel.py
+++ b/src/context/service/database/models/ConstraintModel.py
@@ -30,6 +30,7 @@ class ConstraintKindEnum(enum.Enum):
SLA_LATENCY = 'sla_latency'
SLA_AVAILABILITY = 'sla_availability'
SLA_ISOLATION = 'sla_isolation'
+ EXCLUSIONS = 'exclusions'
class ServiceConstraintModel(_Base):
__tablename__ = 'service_constraint'
diff --git a/src/context/service/database/models/DeviceModel.py b/src/context/service/database/models/DeviceModel.py
index beb500d601aa725c5c0d3c01633aebf31aa23e5b..1097d0b9ab47a86c47ce2ad8394d067ae9f9953e 100644
--- a/src/context/service/database/models/DeviceModel.py
+++ b/src/context/service/database/models/DeviceModel.py
@@ -13,7 +13,7 @@
# limitations under the License.
import operator
-from sqlalchemy import Column, DateTime, Enum, String
+from sqlalchemy import Column, DateTime, Enum, ForeignKey, String
from sqlalchemy.dialects.postgresql import ARRAY, UUID
from sqlalchemy.orm import relationship
from typing import Dict, List
@@ -29,16 +29,22 @@ class DeviceModel(_Base):
device_type = Column(String, nullable=False)
device_operational_status = Column(Enum(ORM_DeviceOperationalStatusEnum), nullable=False)
device_drivers = Column(ARRAY(Enum(ORM_DeviceDriverEnum), dimensions=1))
+ controller_uuid = Column(UUID(as_uuid=False), ForeignKey('device.device_uuid'), nullable=True)
created_at = Column(DateTime, nullable=False)
updated_at = Column(DateTime, nullable=False)
#topology_devices = relationship('TopologyDeviceModel', back_populates='device')
config_rules = relationship('DeviceConfigRuleModel', passive_deletes=True) # lazy='joined', back_populates='device'
endpoints = relationship('EndPointModel', passive_deletes=True) # lazy='joined', back_populates='device'
+ controller = relationship('DeviceModel', remote_side=[device_uuid], passive_deletes=True) # lazy='joined', back_populates='device'
def dump_id(self) -> Dict:
return {'device_uuid': {'uuid': self.device_uuid}}
+ def dump_controller(self) -> Dict:
+ if self.controller is None: return {}
+ return self.controller.dump_id()
+
def dump_endpoints(self) -> List[Dict]:
return [endpoint.dump() for endpoint in self.endpoints]
@@ -60,6 +66,7 @@ class DeviceModel(_Base):
'device_type' : self.device_type,
'device_operational_status': self.device_operational_status.value,
'device_drivers' : [driver.value for driver in self.device_drivers],
+ 'controller_id' : self.dump_controller(),
}
if include_endpoints: result['device_endpoints'] = self.dump_endpoints()
if include_config_rules: result['device_config'] = self.dump_config_rules()
diff --git a/src/context/service/database/models/enums/ServiceStatus.py b/src/context/service/database/models/enums/ServiceStatus.py
index 00ae71f7460fd76a3b8e0f3a981d2e2d08f89e7b..cd2a183b825eff54a51a844ea6834263bbabbc31 100644
--- a/src/context/service/database/models/enums/ServiceStatus.py
+++ b/src/context/service/database/models/enums/ServiceStatus.py
@@ -20,7 +20,9 @@ class ORM_ServiceStatusEnum(enum.Enum):
UNDEFINED = ServiceStatusEnum.SERVICESTATUS_UNDEFINED
PLANNED = ServiceStatusEnum.SERVICESTATUS_PLANNED
ACTIVE = ServiceStatusEnum.SERVICESTATUS_ACTIVE
+ UPDATING = ServiceStatusEnum.SERVICESTATUS_UPDATING
PENDING_REMOVAL = ServiceStatusEnum.SERVICESTATUS_PENDING_REMOVAL
+ SLA_VIOLATED = ServiceStatusEnum.SERVICESTATUS_SLA_VIOLATED
grpc_to_enum__service_status = functools.partial(
grpc_to_enum, ServiceStatusEnum, ORM_ServiceStatusEnum)
diff --git a/src/device/service/DeviceServiceServicerImpl.py b/src/device/service/DeviceServiceServicerImpl.py
index 38a6b735b32ee667c3be2f5381df84c40d773c06..d29d469cb0812218030698284abbfc7551058411 100644
--- a/src/device/service/DeviceServiceServicerImpl.py
+++ b/src/device/service/DeviceServiceServicerImpl.py
@@ -12,9 +12,11 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-import grpc, logging, time
+import grpc, logging, os, time
from typing import Dict
from prometheus_client import Histogram
+from common.Constants import ServiceNameEnum
+from common.Settings import ENVVAR_SUFIX_SERVICE_HOST, get_env_var_name
from common.method_wrappers.Decorator import MetricTypeEnum, MetricsPool, safe_and_metered_rpc_method
from common.method_wrappers.ServiceExceptions import NotFoundException, OperationFailedException
from common.proto.context_pb2 import (
@@ -121,7 +123,15 @@ class DeviceServiceServicerImpl(DeviceServiceServicer):
t9 = time.time()
- device.device_operational_status = DeviceOperationalStatusEnum.DEVICEOPERATIONALSTATUS_DISABLED
+ automation_service_host = get_env_var_name(ServiceNameEnum.AUTOMATION, ENVVAR_SUFIX_SERVICE_HOST)
+ environment_variables = set(os.environ.keys())
+ if automation_service_host in environment_variables:
+ # Automation component is deployed; leave devices disabled. Automation will enable them.
+ device.device_operational_status = DeviceOperationalStatusEnum.DEVICEOPERATIONALSTATUS_DISABLED
+ else:
+ # Automation is not deployed; assume the device is ready while onboarding and set them as enabled.
+ device.device_operational_status = DeviceOperationalStatusEnum.DEVICEOPERATIONALSTATUS_ENABLED
+
device_id = context_client.SetDevice(device)
t10 = time.time()
diff --git a/src/device/service/Tools.py b/src/device/service/Tools.py
index cd3af07e3324e50ff43eb5e653c4c46771a5507e..6a62a75e71f0e02adb7fb1b70e4568b382494980 100644
--- a/src/device/service/Tools.py
+++ b/src/device/service/Tools.py
@@ -79,11 +79,13 @@ def check_no_endpoints(device_endpoints) -> None:
'interrogation of the physical device.')
def get_device_controller_uuid(device : Device) -> Optional[str]:
- for config_rule in device.device_config.config_rules:
- if config_rule.WhichOneof('config_rule') != 'custom': continue
- if config_rule.custom.resource_key != '_controller': continue
- device_controller_id = json.loads(config_rule.custom.resource_value)
- return device_controller_id['uuid']
+ controller_uuid = device.controller_id.device_uuid.uuid
+ if len(controller_uuid) > 0: return controller_uuid
+ #for config_rule in device.device_config.config_rules:
+ # if config_rule.WhichOneof('config_rule') != 'custom': continue
+ # if config_rule.custom.resource_key != '_controller': continue
+ # device_controller_id = json.loads(config_rule.custom.resource_value)
+ # return device_controller_id['uuid']
return None
def populate_endpoints(
@@ -142,11 +144,12 @@ def populate_endpoints(
# Sub-devices should not have a driver assigned. Instead, they should have
# a config rule specifying their controller.
#_sub_device.device_drivers.extend(resource_value['drivers']) # pylint: disable=no-member
- controller_config_rule = _sub_device.device_config.config_rules.add()
- controller_config_rule.action = ConfigActionEnum.CONFIGACTION_SET
- controller_config_rule.custom.resource_key = '_controller'
- controller = {'uuid': device_uuid, 'name': device_name}
- controller_config_rule.custom.resource_value = json.dumps(controller, indent=0, sort_keys=True)
+ #controller_config_rule = _sub_device.device_config.config_rules.add()
+ #controller_config_rule.action = ConfigActionEnum.CONFIGACTION_SET
+ #controller_config_rule.custom.resource_key = '_controller'
+ #controller = {'uuid': device_uuid, 'name': device_name}
+ #controller_config_rule.custom.resource_value = json.dumps(controller, indent=0, sort_keys=True)
+ _sub_device.controller_id.device_uuid.uuid = device_uuid
new_sub_devices[_sub_device_uuid] = _sub_device
diff --git a/src/pathcomp/frontend/service/algorithms/KShortestPathAlgorithm.py b/src/pathcomp/frontend/service/algorithms/KShortestPathAlgorithm.py
index 920d72e828f6f84bc064f1c7357105907ffdac4c..e0fbbe08a1c01402573333f89b1118b6618cc7ce 100644
--- a/src/pathcomp/frontend/service/algorithms/KShortestPathAlgorithm.py
+++ b/src/pathcomp/frontend/service/algorithms/KShortestPathAlgorithm.py
@@ -26,4 +26,5 @@ class KShortestPathAlgorithm(_Algorithm):
for service_request in self.service_list:
service_request['algId' ] = self.algorithm_id
service_request['syncPaths'] = self.sync_paths
- service_request['kPaths' ] = self.k_return
+ service_request['kPaths_inspection'] = self.k_inspection
+ service_request['kPaths_return' ] = self.k_return
diff --git a/src/pathcomp/frontend/service/algorithms/_Algorithm.py b/src/pathcomp/frontend/service/algorithms/_Algorithm.py
index b486ec1b59457b1ac575fb6197c7713b10c306e3..0eb01c1341421e379850f89d5671d9156c8a9fd6 100644
--- a/src/pathcomp/frontend/service/algorithms/_Algorithm.py
+++ b/src/pathcomp/frontend/service/algorithms/_Algorithm.py
@@ -253,6 +253,7 @@ class _Algorithm:
for connection in connections:
connection_uuid,service_type,path_hops,_ = connection
service_key = (context_uuid, connection_uuid)
+ if service_key in grpc_services: continue
grpc_service = self.add_service_to_reply(
reply, context_uuid, connection_uuid, service_type, path_hops=path_hops,
config_rules=orig_config_rules)
@@ -265,10 +266,9 @@ class _Algorithm:
grpc_service = grpc_services.get(service_key)
if grpc_service is None: raise Exception('Service({:s}) not found'.format(str(service_key)))
- grpc_connection = grpc_connections.get(connection_uuid)
- if grpc_connection is not None: continue
+ #if connection_uuid in grpc_connections: continue
grpc_connection = self.add_connection_to_reply(reply, connection_uuid, grpc_service, path_hops)
- grpc_connections[connection_uuid] = grpc_connection
+ #grpc_connections[connection_uuid] = grpc_connection
for sub_service_uuid in dependencies:
sub_service_key = (context_uuid, sub_service_uuid)
@@ -281,11 +281,11 @@ class _Algorithm:
# ... "path-capacity": {"total-size": {"value": 200, "unit": 0}},
# ... "path-latency": {"fixed-latency-characteristic": "10.000000"},
# ... "path-cost": {"cost-name": "", "cost-value": "5.000000", "cost-algorithm": "0.000000"},
- #path_capacity = service_path['path-capacity']['total-size']
+ #path_capacity = service_path_ero['path-capacity']['total-size']
#path_capacity_value = path_capacity['value']
#path_capacity_unit = CapacityUnit(path_capacity['unit'])
- #path_latency = service_path['path-latency']['fixed-latency-characteristic']
- #path_cost = service_path['path-cost']
+ #path_latency = service_path_ero['path-latency']['fixed-latency-characteristic']
+ #path_cost = service_path_ero['path-cost']
#path_cost_name = path_cost['cost-name']
#path_cost_value = path_cost['cost-value']
#path_cost_algorithm = path_cost['cost-algorithm']
diff --git a/src/pathcomp/frontend/service/algorithms/tools/ResourceGroups.py b/src/pathcomp/frontend/service/algorithms/tools/ResourceGroups.py
index e56d436dd006197497d7774be598a480a134320c..c1591dbeb7c71c950135b92446849569bcd781f8 100644
--- a/src/pathcomp/frontend/service/algorithms/tools/ResourceGroups.py
+++ b/src/pathcomp/frontend/service/algorithms/tools/ResourceGroups.py
@@ -36,8 +36,8 @@ DEVICE_TYPE_TO_DEEPNESS = {
DeviceTypeEnum.EMULATED_XR_CONSTELLATION.value : 40,
DeviceTypeEnum.XR_CONSTELLATION.value : 40,
- DeviceTypeEnum.EMULATED_MICROWAVE_RADIO_SYSTEM.value : 30,
- DeviceTypeEnum.MICROWAVE_RADIO_SYSTEM.value : 30,
+ DeviceTypeEnum.EMULATED_MICROWAVE_RADIO_SYSTEM.value : 40,
+ DeviceTypeEnum.MICROWAVE_RADIO_SYSTEM.value : 40,
DeviceTypeEnum.EMULATED_OPEN_LINE_SYSTEM.value : 30,
DeviceTypeEnum.OPEN_LINE_SYSTEM.value : 30,
@@ -57,11 +57,13 @@ IGNORED_DEVICE_TYPES = {DeviceTypeEnum.EMULATED_OPTICAL_SPLITTER}
def get_device_controller_uuid(
device : Device
) -> Optional[str]:
- for config_rule in device.device_config.config_rules:
- if config_rule.WhichOneof('config_rule') != 'custom': continue
- if config_rule.custom.resource_key != '_controller': continue
- device_controller_id = json.loads(config_rule.custom.resource_value)
- return device_controller_id['uuid']
+ controller_uuid = device.controller_id.device_uuid.uuid
+ if len(controller_uuid) > 0: return controller_uuid
+ #for config_rule in device.device_config.config_rules:
+ # if config_rule.WhichOneof('config_rule') != 'custom': continue
+ # if config_rule.custom.resource_key != '_controller': continue
+ # device_controller_id = json.loads(config_rule.custom.resource_value)
+ # return device_controller_id['uuid']
return None
def _map_device_type(device : Device) -> DeviceTypeEnum:
diff --git a/src/policy/src/main/java/eu/teraflow/policy/Serializer.java b/src/policy/src/main/java/eu/teraflow/policy/Serializer.java
index 967d1d6e604e312fe9d8314beea023f902af776b..52d594ea4200c2ce4d775edf2f06cf7a9c9f9097 100644
--- a/src/policy/src/main/java/eu/teraflow/policy/Serializer.java
+++ b/src/policy/src/main/java/eu/teraflow/policy/Serializer.java
@@ -1124,8 +1124,12 @@ public class Serializer {
return ContextOuterClass.ServiceStatusEnum.SERVICESTATUS_PLANNED;
case PENDING_REMOVAL:
return ContextOuterClass.ServiceStatusEnum.SERVICESTATUS_PENDING_REMOVAL;
+ case SLA_VIOLATED:
+ return ContextOuterClass.ServiceStatusEnum.SERVICESTATUS_SLA_VIOLATED;
case UNDEFINED:
return ContextOuterClass.ServiceStatusEnum.SERVICESTATUS_UNDEFINED;
+ case UPDATING:
+ return ContextOuterClass.ServiceStatusEnum.SERVICESTATUS_UPDATING;
default:
return ContextOuterClass.ServiceStatusEnum.UNRECOGNIZED;
}
@@ -1140,6 +1144,10 @@ public class Serializer {
return ServiceStatusEnum.PLANNED;
case SERVICESTATUS_PENDING_REMOVAL:
return ServiceStatusEnum.PENDING_REMOVAL;
+ case SERVICESTATUS_SLA_VIOLATED:
+ return ServiceStatusEnum.SLA_VIOLATED;
+ case SERVICESTATUS_UPDATING:
+ return ServiceStatusEnum.UPDATING;
case SERVICESTATUS_UNDEFINED:
case UNRECOGNIZED:
default:
diff --git a/src/policy/src/test/java/eu/teraflow/policy/SerializerTest.java b/src/policy/src/test/java/eu/teraflow/policy/SerializerTest.java
index b0fb90864ce32bf6b793dded5d1f9de1dfba5097..f06c30204b874cd6be30cd1a906c5087412e9640 100644
--- a/src/policy/src/test/java/eu/teraflow/policy/SerializerTest.java
+++ b/src/policy/src/test/java/eu/teraflow/policy/SerializerTest.java
@@ -1910,6 +1910,12 @@ class SerializerTest {
Arguments.of(
ServiceStatusEnum.PENDING_REMOVAL,
ContextOuterClass.ServiceStatusEnum.SERVICESTATUS_PENDING_REMOVAL),
+ Arguments.of(
+ ServiceStatusEnum.SLA_VIOLATED,
+ ContextOuterClass.ServiceStatusEnum.SERVICESTATUS_SLA_VIOLATED),
+ Arguments.of(
+ ServiceStatusEnum.UPDATING,
+ ContextOuterClass.ServiceStatusEnum.SERVICESTATUS_UPDATING),
Arguments.of(
ServiceStatusEnum.UNDEFINED,
ContextOuterClass.ServiceStatusEnum.SERVICESTATUS_UNDEFINED));
diff --git a/src/service/client/ServiceClient.py b/src/service/client/ServiceClient.py
index 30ff4f4838dd52d7010f08a7814ff208afbe92f4..e8ea478a3109d3e006120db9f22966724773b78b 100644
--- a/src/service/client/ServiceClient.py
+++ b/src/service/client/ServiceClient.py
@@ -65,3 +65,10 @@ class ServiceClient:
response = self.stub.DeleteService(request)
LOGGER.debug('DeleteService result: {:s}'.format(grpc_message_to_json_string(response)))
return response
+
+ @RETRY_DECORATOR
+ def RecomputeConnections(self, request : Service) -> Empty:
+ LOGGER.debug('RecomputeConnections request: {:s}'.format(grpc_message_to_json_string(request)))
+ response = self.stub.RecomputeConnections(request)
+ LOGGER.debug('RecomputeConnections result: {:s}'.format(grpc_message_to_json_string(response)))
+ return response
diff --git a/src/service/service/ServiceServiceServicerImpl.py b/src/service/service/ServiceServiceServicerImpl.py
index 6531376b84732b1ec80e335cfc6cd816be944b0a..6d23fd4cee53d1639c9eefbd943d45dab497b253 100644
--- a/src/service/service/ServiceServiceServicerImpl.py
+++ b/src/service/service/ServiceServiceServicerImpl.py
@@ -12,17 +12,20 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-import grpc, json, logging
+import 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
-from common.proto.context_pb2 import Empty, Service, ServiceId, ServiceStatusEnum, ServiceTypeEnum
+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.pathcomp_pb2 import PathCompRequest
from common.proto.service_pb2_grpc import ServiceServiceServicer
from common.tools.context_queries.Service import get_service_by_id
from common.tools.grpc.Tools import grpc_message_to_json, grpc_message_to_json_string
from context.client.ContextClient import ContextClient
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
@@ -168,3 +171,160 @@ class ServiceServiceServicerImpl(ServiceServiceServicer):
tasks_scheduler.compose_from_service(service, is_delete=True)
tasks_scheduler.execute_all()
return Empty()
+
+ @safe_and_metered_rpc_method(METRICS_POOL, LOGGER)
+ def RecomputeConnections(self, request : Service, context : grpc.ServicerContext) -> Empty:
+ if len(request.service_endpoint_ids) > 0:
+ raise NotImplementedException('update-endpoints')
+
+ if len(request.service_constraints) > 0:
+ raise NotImplementedException('update-constraints')
+
+ if len(request.service_config.config_rules) > 0:
+ raise NotImplementedException('update-config-rules')
+
+ context_client = ContextClient()
+
+ updated_service : Optional[Service] = get_service_by_id(
+ context_client, request.service_id, rw_copy=True,
+ include_config_rules=False, include_constraints=False, include_endpoint_ids=False)
+
+ if updated_service is None:
+ raise NotFoundException('service', request.service_id.service_uuid.uuid)
+
+ # pylint: disable=no-member
+ if updated_service.service_type == ServiceTypeEnum.SERVICETYPE_UNKNOWN:
+ raise InvalidArgumentException(
+ 'request.service_type', ServiceTypeEnum.Name(updated_service.service_type)
+ )
+
+ # Set service status to "SERVICESTATUS_UPDATING" to ensure rest of components are aware the service is
+ # being modified.
+ # pylint: disable=no-member
+ updated_service.service_status.service_status = ServiceStatusEnum.SERVICESTATUS_UPDATING
+
+ # Update endpoints
+ # pylint: disable=no-member
+ #del updated_service.service_endpoint_ids[:]
+ #updated_service.service_endpoint_ids.extend(request.service_endpoint_ids)
+
+ # Update constraints
+ # pylint: disable=no-member
+ #del updated_service.service_constraints[:]
+ #updated_service.service_constraints.extend(request.service_constraints)
+
+ # Update config rules
+ # pylint: disable=no-member
+ #del updated_service.service_config.config_rules[:]
+ #updated_service.service_config.config_rules.extend(request.service_config.config_rules)
+
+ updated_service_id_with_uuids = context_client.SetService(updated_service)
+
+ # PathComp requires endpoints, constraints and config rules
+ updated_service_with_uuids = get_service_by_id(
+ context_client, updated_service_id_with_uuids, rw_copy=True,
+ include_config_rules=True, include_constraints=True, include_endpoint_ids=True)
+
+ # Get active connection
+ connections = context_client.ListConnections(updated_service_id_with_uuids)
+ if len(connections.connections) == 0:
+ MSG = 'Service({:s}) has no connections'
+ str_service_id = grpc_message_to_json_string(updated_service_id_with_uuids)
+ str_extra_details = MSG.format(str_service_id)
+ raise NotImplementedException('service-with-no-connections', extra_details=str_extra_details)
+ if len(connections.connections) > 1:
+ MSG = 'Service({:s}) has multiple ({:d}) connections({:s})'
+ str_service_id = grpc_message_to_json_string(updated_service_id_with_uuids)
+ num_connections = len(connections.connections)
+ str_connections = grpc_message_to_json_string(connections)
+ str_extra_details = MSG.format(str_service_id, num_connections, str_connections)
+ raise NotImplementedException('service-with-multiple-connections', extra_details=str_extra_details)
+
+ old_connection = connections.connections[0]
+ if len(old_connection.sub_service_ids) > 0:
+ MSG = 'Service({:s})/Connection({:s}) has sub-services: {:s}'
+ str_service_id = grpc_message_to_json_string(updated_service_id_with_uuids)
+ str_connection_id = grpc_message_to_json_string(old_connection.connection_id)
+ str_connection = grpc_message_to_json_string(old_connection)
+ str_extra_details = MSG.format(str_service_id, str_connection_id, str_connection)
+ raise NotImplementedException('service-connection-with-subservices', extra_details=str_extra_details)
+
+ # Find alternative connections
+ # pylint: disable=no-member
+ pathcomp_request = PathCompRequest()
+ pathcomp_request.services.append(updated_service_with_uuids)
+ #pathcomp_request.k_disjoint_path.num_disjoint = 100
+ pathcomp_request.k_shortest_path.k_inspection = 100
+ pathcomp_request.k_shortest_path.k_return = 3
+
+ LOGGER.debug('pathcomp_request={:s}'.format(grpc_message_to_json_string(pathcomp_request)))
+ pathcomp = PathCompClient()
+ pathcomp_reply = pathcomp.Compute(pathcomp_request)
+ pathcomp.close()
+ LOGGER.debug('pathcomp_reply={:s}'.format(grpc_message_to_json_string(pathcomp_reply)))
+
+ if len(pathcomp_reply.services) == 0:
+ MSG = 'KDisjointPath reported no services for Service({:s}): {:s}'
+ str_service_id = grpc_message_to_json_string(updated_service_id_with_uuids)
+ str_pathcomp_reply = grpc_message_to_json_string(pathcomp_reply)
+ str_extra_details = MSG.format(str_service_id, str_pathcomp_reply)
+ raise NotImplementedException('kdisjointpath-no-services', extra_details=str_extra_details)
+
+ if len(pathcomp_reply.services) > 1:
+ MSG = 'KDisjointPath reported subservices for Service({:s}): {:s}'
+ str_service_id = grpc_message_to_json_string(updated_service_id_with_uuids)
+ str_pathcomp_reply = grpc_message_to_json_string(pathcomp_reply)
+ str_extra_details = MSG.format(str_service_id, str_pathcomp_reply)
+ raise NotImplementedException('kdisjointpath-subservices', extra_details=str_extra_details)
+
+ if len(pathcomp_reply.connections) == 0:
+ MSG = 'KDisjointPath reported no connections for Service({:s}): {:s}'
+ str_service_id = grpc_message_to_json_string(updated_service_id_with_uuids)
+ str_pathcomp_reply = grpc_message_to_json_string(pathcomp_reply)
+ str_extra_details = MSG.format(str_service_id, str_pathcomp_reply)
+ raise NotImplementedException('kdisjointpath-no-connections', extra_details=str_extra_details)
+
+ # compute a string representing the old connection
+ str_old_connection = connection_to_string(old_connection)
+
+ LOGGER.debug('old_connection={:s}'.format(grpc_message_to_json_string(old_connection)))
+
+ candidate_new_connections = list()
+ for candidate_new_connection in pathcomp_reply.connections:
+ str_candidate_new_connection = connection_to_string(candidate_new_connection)
+ if str_candidate_new_connection == str_old_connection: continue
+ candidate_new_connections.append(candidate_new_connection)
+
+ if len(candidate_new_connections) == 0:
+ MSG = 'Unable to find a new suitable path: pathcomp_request={:s} pathcomp_reply={:s} old_connection={:s}'
+ str_pathcomp_request = grpc_message_to_json_string(pathcomp_request)
+ str_pathcomp_reply = grpc_message_to_json_string(pathcomp_reply)
+ str_old_connection = grpc_message_to_json_string(old_connection)
+ extra_details = MSG.format(str_pathcomp_request, str_pathcomp_reply, str_old_connection)
+ raise OperationFailedException('no-new-path-found', extra_details=extra_details)
+
+ str_candidate_new_connections = [
+ grpc_message_to_json_string(candidate_new_connection)
+ for candidate_new_connection in candidate_new_connections
+ ]
+ LOGGER.debug('candidate_new_connections={:s}'.format(str(str_candidate_new_connections)))
+
+ new_connection = random.choice(candidate_new_connections)
+ LOGGER.debug('new_connection={:s}'.format(grpc_message_to_json_string(new_connection)))
+
+ # Change UUID of new connection to prevent collisions
+ tmp_connection = Connection()
+ tmp_connection.CopyFrom(new_connection)
+ tmp_connection.connection_id.connection_uuid.uuid = str(uuid.uuid4())
+ new_connection = tmp_connection
+
+ # Feed TaskScheduler with the service to update, the old connection to
+ # deconfigure and the new connection to configure. It will produce a
+ # schedule of tasks (an ordered list of tasks to be executed) to
+ # implement the requested changes.
+ tasks_scheduler = TasksScheduler(self.service_handler_factory)
+ tasks_scheduler.compose_service_connection_update(
+ updated_service_with_uuids, old_connection, new_connection)
+ tasks_scheduler.execute_all()
+
+ return Empty()
diff --git a/src/service/service/task_scheduler/TaskExecutor.py b/src/service/service/task_scheduler/TaskExecutor.py
index acda45ce80a62a4a3723744546968e3195799b59..ae0f1be7da291a5dc025641cb606f7a7706059ca 100644
--- a/src/service/service/task_scheduler/TaskExecutor.py
+++ b/src/service/service/task_scheduler/TaskExecutor.py
@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-import json, logging
+import logging #, json
from enum import Enum
from typing import TYPE_CHECKING, Any, Dict, Optional, Union
from common.method_wrappers.ServiceExceptions import NotFoundException
@@ -20,7 +20,7 @@ from common.proto.context_pb2 import Connection, ConnectionId, Device, DeviceDri
from common.tools.context_queries.Connection import get_connection_by_id
from common.tools.context_queries.Device import get_device
from common.tools.context_queries.Service import get_service_by_id
-from common.tools.grpc.Tools import grpc_message_list_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.client.ContextClient import ContextClient
from device.client.DeviceClient import DeviceClient
@@ -113,16 +113,18 @@ class TaskExecutor:
self._store_grpc_object(CacheableObjectType.DEVICE, device_key, device)
def get_device_controller(self, device : Device) -> Optional[Device]:
- json_controller = None
- for config_rule in device.device_config.config_rules:
- if config_rule.WhichOneof('config_rule') != 'custom': continue
- if config_rule.custom.resource_key != '_controller': continue
- json_controller = json.loads(config_rule.custom.resource_value)
- break
-
- if json_controller is None: return None
-
- controller_uuid = json_controller['uuid']
+ #json_controller = None
+ #for config_rule in device.device_config.config_rules:
+ # if config_rule.WhichOneof('config_rule') != 'custom': continue
+ # if config_rule.custom.resource_key != '_controller': continue
+ # json_controller = json.loads(config_rule.custom.resource_value)
+ # break
+
+ #if json_controller is None: return None
+
+ #controller_uuid = json_controller['uuid']
+ controller_uuid = device.controller_id.device_uuid.uuid
+ if len(controller_uuid) == 0: return None
controller = self.get_device(DeviceId(**json_device_id(controller_uuid)))
controller_uuid = controller.device_id.device_uuid.uuid
if controller is None: raise Exception('Device({:s}) not found'.format(str(controller_uuid)))
@@ -188,7 +190,7 @@ class TaskExecutor:
}
LOGGER.exception(
'Unable to select service handler. service={:s} connection={:s} connection_devices={:s}'.format(
- grpc_message_list_to_json_string(service), grpc_message_list_to_json_string(connection),
+ grpc_message_to_json_string(service), grpc_message_to_json_string(connection),
str(dict_connection_devices)
)
)
diff --git a/src/service/service/task_scheduler/TaskScheduler.py b/src/service/service/task_scheduler/TaskScheduler.py
index fbc554aa261cbc68009258d322aa01d52bfe760d..fceed36e92771394dff9e9f45ef928a0175b8d32 100644
--- a/src/service/service/task_scheduler/TaskScheduler.py
+++ b/src/service/service/task_scheduler/TaskScheduler.py
@@ -198,15 +198,57 @@ class TasksScheduler:
t1 = time.time()
LOGGER.debug('[compose_from_service] elapsed_time: {:f} sec'.format(t1-t0))
+ def compose_service_connection_update(
+ self, service : Service, old_connection : Connection, new_connection : Connection
+ ) -> None:
+ t0 = time.time()
+
+ self._add_service_to_executor_cache(service)
+ self._add_connection_to_executor_cache(old_connection)
+ self._add_connection_to_executor_cache(new_connection)
+
+ service_updating_key = self._add_task_if_not_exists(Task_ServiceSetStatus(
+ self._executor, service.service_id, ServiceStatusEnum.SERVICESTATUS_UPDATING))
+
+ old_connection_deconfigure_key = self._add_task_if_not_exists(Task_ConnectionDeconfigure(
+ self._executor, old_connection.connection_id))
+
+ new_connection_configure_key = self._add_task_if_not_exists(Task_ConnectionConfigure(
+ self._executor, new_connection.connection_id))
+
+ service_active_key = self._add_task_if_not_exists(Task_ServiceSetStatus(
+ self._executor, service.service_id, ServiceStatusEnum.SERVICESTATUS_ACTIVE))
+
+ # the old connection deconfiguration depends on service being in updating state
+ self._dag.add(old_connection_deconfigure_key, service_updating_key)
+
+ # the new connection configuration depends on service being in updating state
+ self._dag.add(new_connection_configure_key, service_updating_key)
+
+ # the new connection configuration depends on the old connection having been deconfigured
+ self._dag.add(new_connection_configure_key, old_connection_deconfigure_key)
+
+ # re-activating the service depends on the service being in updating state before
+ self._dag.add(service_active_key, service_updating_key)
+
+ # re-activating the service depends on the new connection having been configured
+ self._dag.add(service_active_key, new_connection_configure_key)
+
+ t1 = time.time()
+ LOGGER.debug('[compose_service_connection_update] elapsed_time: {:f} sec'.format(t1-t0))
+
def execute_all(self, dry_run : bool = False) -> None:
ordered_task_keys = list(self._dag.static_order())
LOGGER.debug('[execute_all] ordered_task_keys={:s}'.format(str(ordered_task_keys)))
results = []
for task_key in ordered_task_keys:
+ str_task_name = ('DRY ' if dry_run else '') + str(task_key)
+ LOGGER.debug('[execute_all] starting task {:s}'.format(str_task_name))
task = self._tasks.get(task_key)
succeeded = True if dry_run else task.execute()
results.append(succeeded)
+ LOGGER.debug('[execute_all] finished task {:s} ; succeeded={:s}'.format(str_task_name, str(succeeded)))
LOGGER.debug('[execute_all] results={:s}'.format(str(results)))
return zip(ordered_task_keys, results)
diff --git a/src/service/service/tools/ConnectionToString.py b/src/service/service/tools/ConnectionToString.py
new file mode 100644
index 0000000000000000000000000000000000000000..1c189e00ff9004dc0929f58a02560e8bea69fa91
--- /dev/null
+++ b/src/service/service/tools/ConnectionToString.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.
+
+from typing import List
+from common.proto.context_pb2 import Connection
+
+def connection_to_string(connection : Connection) -> str:
+ str_device_endpoint_uuids : List[str] = list()
+ for endpoint_id in connection.path_hops_endpoint_ids:
+ device_uuid = endpoint_id.device_id.device_uuid.uuid
+ endpoint_uuid = endpoint_id.endpoint_uuid.uuid
+ device_endpoint_uuid = '{:s}:{:s}'.format(device_uuid, endpoint_uuid)
+ str_device_endpoint_uuids.append(device_endpoint_uuid)
+ return ','.join(str_device_endpoint_uuids)
diff --git a/src/service/tests/descriptors_recompute_conns.json b/src/service/tests/descriptors_recompute_conns.json
new file mode 100644
index 0000000000000000000000000000000000000000..dd571ccb6b2ff61ca0e581780ca02d71171bb894
--- /dev/null
+++ b/src/service/tests/descriptors_recompute_conns.json
@@ -0,0 +1,239 @@
+{
+ "contexts": [
+ {"context_id": {"context_uuid": {"uuid": "admin"}}}
+ ],
+ "topologies": [
+ {"topology_id": {"context_id": {"context_uuid": {"uuid": "admin"}}, "topology_uuid": {"uuid": "admin"}}}
+ ],
+ "devices": [
+ {
+ "device_id": {"device_uuid": {"uuid": "R1"}}, "device_type": "emu-packet-router", "device_drivers": [0],
+ "device_endpoints": [], "device_operational_status": 1, "device_config": {"config_rules": [
+ {"action": 1, "custom": {"resource_key": "_connect/address", "resource_value": "127.0.0.1"}},
+ {"action": 1, "custom": {"resource_key": "_connect/port", "resource_value": "0"}},
+ {"action": 1, "custom": {"resource_key": "_connect/settings", "resource_value": {"endpoints": [
+ {"sample_types": [], "type": "copper", "uuid": "1/1"},
+ {"sample_types": [], "type": "copper", "uuid": "1/2"},
+ {"sample_types": [], "type": "copper", "uuid": "1/3"},
+ {"sample_types": [], "type": "copper", "uuid": "1/4"},
+ {"sample_types": [], "type": "copper", "uuid": "1/5"},
+ {"sample_types": [], "type": "copper", "uuid": "1/6"},
+ {"sample_types": [], "type": "copper", "uuid": "2/1"},
+ {"sample_types": [], "type": "copper", "uuid": "2/2"},
+ {"sample_types": [], "type": "copper", "uuid": "2/3"}
+ ]}}}
+ ]}
+ },
+ {
+ "device_id": {"device_uuid": {"uuid": "R2"}}, "device_type": "emu-packet-router", "device_drivers": [0],
+ "device_endpoints": [], "device_operational_status": 1, "device_config": {"config_rules": [
+ {"action": 1, "custom": {"resource_key": "_connect/address", "resource_value": "127.0.0.1"}},
+ {"action": 1, "custom": {"resource_key": "_connect/port", "resource_value": "0"}},
+ {"action": 1, "custom": {"resource_key": "_connect/settings", "resource_value": {"endpoints": [
+ {"sample_types": [], "type": "copper", "uuid": "1/1"},
+ {"sample_types": [], "type": "copper", "uuid": "1/2"},
+ {"sample_types": [], "type": "copper", "uuid": "1/3"},
+ {"sample_types": [], "type": "copper", "uuid": "1/4"},
+ {"sample_types": [], "type": "copper", "uuid": "1/5"},
+ {"sample_types": [], "type": "copper", "uuid": "1/6"},
+ {"sample_types": [], "type": "copper", "uuid": "2/1"},
+ {"sample_types": [], "type": "copper", "uuid": "2/2"},
+ {"sample_types": [], "type": "copper", "uuid": "2/3"}
+ ]}}}
+ ]}
+ },
+ {
+ "device_id": {"device_uuid": {"uuid": "R3"}}, "device_type": "emu-packet-router", "device_drivers": [0],
+ "device_endpoints": [], "device_operational_status": 1, "device_config": {"config_rules": [
+ {"action": 1, "custom": {"resource_key": "_connect/address", "resource_value": "127.0.0.1"}},
+ {"action": 1, "custom": {"resource_key": "_connect/port", "resource_value": "0"}},
+ {"action": 1, "custom": {"resource_key": "_connect/settings", "resource_value": {"endpoints": [
+ {"sample_types": [], "type": "copper", "uuid": "1/1"},
+ {"sample_types": [], "type": "copper", "uuid": "1/2"},
+ {"sample_types": [], "type": "copper", "uuid": "1/3"},
+ {"sample_types": [], "type": "copper", "uuid": "1/4"},
+ {"sample_types": [], "type": "copper", "uuid": "1/5"},
+ {"sample_types": [], "type": "copper", "uuid": "1/6"},
+ {"sample_types": [], "type": "copper", "uuid": "2/1"},
+ {"sample_types": [], "type": "copper", "uuid": "2/2"},
+ {"sample_types": [], "type": "copper", "uuid": "2/3"}
+ ]}}}
+ ]}
+ },
+ {
+ "device_id": {"device_uuid": {"uuid": "R4"}}, "device_type": "emu-packet-router", "device_drivers": [0],
+ "device_endpoints": [], "device_operational_status": 1, "device_config": {"config_rules": [
+ {"action": 1, "custom": {"resource_key": "_connect/address", "resource_value": "127.0.0.1"}},
+ {"action": 1, "custom": {"resource_key": "_connect/port", "resource_value": "0"}},
+ {"action": 1, "custom": {"resource_key": "_connect/settings", "resource_value": {"endpoints": [
+ {"sample_types": [], "type": "copper", "uuid": "1/1"},
+ {"sample_types": [], "type": "copper", "uuid": "1/2"},
+ {"sample_types": [], "type": "copper", "uuid": "1/3"},
+ {"sample_types": [], "type": "copper", "uuid": "1/4"},
+ {"sample_types": [], "type": "copper", "uuid": "1/5"},
+ {"sample_types": [], "type": "copper", "uuid": "1/6"},
+ {"sample_types": [], "type": "copper", "uuid": "2/1"},
+ {"sample_types": [], "type": "copper", "uuid": "2/2"},
+ {"sample_types": [], "type": "copper", "uuid": "2/3"}
+ ]}}}
+ ]}
+ },
+ {
+ "device_id": {"device_uuid": {"uuid": "R5"}}, "device_type": "emu-packet-router", "device_drivers": [0],
+ "device_endpoints": [], "device_operational_status": 1, "device_config": {"config_rules": [
+ {"action": 1, "custom": {"resource_key": "_connect/address", "resource_value": "127.0.0.1"}},
+ {"action": 1, "custom": {"resource_key": "_connect/port", "resource_value": "0"}},
+ {"action": 1, "custom": {"resource_key": "_connect/settings", "resource_value": {"endpoints": [
+ {"sample_types": [], "type": "copper", "uuid": "1/1"},
+ {"sample_types": [], "type": "copper", "uuid": "1/2"},
+ {"sample_types": [], "type": "copper", "uuid": "1/3"},
+ {"sample_types": [], "type": "copper", "uuid": "1/4"},
+ {"sample_types": [], "type": "copper", "uuid": "1/5"},
+ {"sample_types": [], "type": "copper", "uuid": "1/6"},
+ {"sample_types": [], "type": "copper", "uuid": "2/1"},
+ {"sample_types": [], "type": "copper", "uuid": "2/2"},
+ {"sample_types": [], "type": "copper", "uuid": "2/3"}
+ ]}}}
+ ]}
+ },
+ {
+ "device_id": {"device_uuid": {"uuid": "R6"}}, "device_type": "emu-packet-router", "device_drivers": [0],
+ "device_endpoints": [], "device_operational_status": 1, "device_config": {"config_rules": [
+ {"action": 1, "custom": {"resource_key": "_connect/address", "resource_value": "127.0.0.1"}},
+ {"action": 1, "custom": {"resource_key": "_connect/port", "resource_value": "0"}},
+ {"action": 1, "custom": {"resource_key": "_connect/settings", "resource_value": {"endpoints": [
+ {"sample_types": [], "type": "copper", "uuid": "1/1"},
+ {"sample_types": [], "type": "copper", "uuid": "1/2"},
+ {"sample_types": [], "type": "copper", "uuid": "1/3"},
+ {"sample_types": [], "type": "copper", "uuid": "1/4"},
+ {"sample_types": [], "type": "copper", "uuid": "1/5"},
+ {"sample_types": [], "type": "copper", "uuid": "1/6"},
+ {"sample_types": [], "type": "copper", "uuid": "2/1"},
+ {"sample_types": [], "type": "copper", "uuid": "2/2"},
+ {"sample_types": [], "type": "copper", "uuid": "2/3"}
+ ]}}}
+ ]}
+ },
+ {
+ "device_id": {"device_uuid": {"uuid": "R7"}}, "device_type": "emu-packet-router", "device_drivers": [0],
+ "device_endpoints": [], "device_operational_status": 1, "device_config": {"config_rules": [
+ {"action": 1, "custom": {"resource_key": "_connect/address", "resource_value": "127.0.0.1"}},
+ {"action": 1, "custom": {"resource_key": "_connect/port", "resource_value": "0"}},
+ {"action": 1, "custom": {"resource_key": "_connect/settings", "resource_value": {"endpoints": [
+ {"sample_types": [], "type": "copper", "uuid": "1/1"},
+ {"sample_types": [], "type": "copper", "uuid": "1/2"},
+ {"sample_types": [], "type": "copper", "uuid": "1/3"},
+ {"sample_types": [], "type": "copper", "uuid": "2/1"},
+ {"sample_types": [], "type": "copper", "uuid": "2/2"},
+ {"sample_types": [], "type": "copper", "uuid": "2/3"},
+ {"sample_types": [], "type": "copper", "uuid": "2/4"},
+ {"sample_types": [], "type": "copper", "uuid": "2/5"},
+ {"sample_types": [], "type": "copper", "uuid": "2/6"}
+ ]}}}
+ ]}
+ }
+ ],
+ "links": [
+ {"link_id": {"link_uuid": {"uuid": "R1==R2"}}, "link_endpoint_ids": [
+ {"device_id": {"device_uuid": {"uuid": "R1"}}, "endpoint_uuid": {"uuid": "2/1"}},
+ {"device_id": {"device_uuid": {"uuid": "R2"}}, "endpoint_uuid": {"uuid": "2/2"}}
+ ]},
+ {"link_id": {"link_uuid": {"uuid": "R1==R6"}}, "link_endpoint_ids": [
+ {"device_id": {"device_uuid": {"uuid": "R1"}}, "endpoint_uuid": {"uuid": "2/2"}},
+ {"device_id": {"device_uuid": {"uuid": "R6"}}, "endpoint_uuid": {"uuid": "2/1"}}
+ ]},
+ {"link_id": {"link_uuid": {"uuid": "R1==R7"}}, "link_endpoint_ids": [
+ {"device_id": {"device_uuid": {"uuid": "R1"}}, "endpoint_uuid": {"uuid": "2/3"}},
+ {"device_id": {"device_uuid": {"uuid": "R7"}}, "endpoint_uuid": {"uuid": "2/1"}}
+ ]},
+ {"link_id": {"link_uuid": {"uuid": "R2==R1"}}, "link_endpoint_ids": [
+ {"device_id": {"device_uuid": {"uuid": "R2"}}, "endpoint_uuid": {"uuid": "2/2"}},
+ {"device_id": {"device_uuid": {"uuid": "R1"}}, "endpoint_uuid": {"uuid": "2/1"}}
+ ]},
+ {"link_id": {"link_uuid": {"uuid": "R2==R3"}}, "link_endpoint_ids": [
+ {"device_id": {"device_uuid": {"uuid": "R2"}}, "endpoint_uuid": {"uuid": "2/1"}},
+ {"device_id": {"device_uuid": {"uuid": "R3"}}, "endpoint_uuid": {"uuid": "2/2"}}
+ ]},
+ {"link_id": {"link_uuid": {"uuid": "R3==R2"}}, "link_endpoint_ids": [
+ {"device_id": {"device_uuid": {"uuid": "R3"}}, "endpoint_uuid": {"uuid": "2/2"}},
+ {"device_id": {"device_uuid": {"uuid": "R2"}}, "endpoint_uuid": {"uuid": "2/1"}}
+ ]},
+ {"link_id": {"link_uuid": {"uuid": "R3==R4"}}, "link_endpoint_ids": [
+ {"device_id": {"device_uuid": {"uuid": "R3"}}, "endpoint_uuid": {"uuid": "2/1"}},
+ {"device_id": {"device_uuid": {"uuid": "R4"}}, "endpoint_uuid": {"uuid": "2/2"}}
+ ]},
+ {"link_id": {"link_uuid": {"uuid": "R3==R7"}}, "link_endpoint_ids": [
+ {"device_id": {"device_uuid": {"uuid": "R3"}}, "endpoint_uuid": {"uuid": "2/3"}},
+ {"device_id": {"device_uuid": {"uuid": "R7"}}, "endpoint_uuid": {"uuid": "2/3"}}
+ ]},
+ {"link_id": {"link_uuid": {"uuid": "R4==R3"}}, "link_endpoint_ids": [
+ {"device_id": {"device_uuid": {"uuid": "R4"}}, "endpoint_uuid": {"uuid": "2/2"}},
+ {"device_id": {"device_uuid": {"uuid": "R3"}}, "endpoint_uuid": {"uuid": "2/1"}}
+ ]},
+ {"link_id": {"link_uuid": {"uuid": "R4==R5"}}, "link_endpoint_ids": [
+ {"device_id": {"device_uuid": {"uuid": "R4"}}, "endpoint_uuid": {"uuid": "2/1"}},
+ {"device_id": {"device_uuid": {"uuid": "R5"}}, "endpoint_uuid": {"uuid": "2/2"}}
+ ]},
+ {"link_id": {"link_uuid": {"uuid": "R5==R4"}}, "link_endpoint_ids": [
+ {"device_id": {"device_uuid": {"uuid": "R5"}}, "endpoint_uuid": {"uuid": "2/2"}},
+ {"device_id": {"device_uuid": {"uuid": "R4"}}, "endpoint_uuid": {"uuid": "2/1"}}
+ ]},
+ {"link_id": {"link_uuid": {"uuid": "R5==R6"}}, "link_endpoint_ids": [
+ {"device_id": {"device_uuid": {"uuid": "R5"}}, "endpoint_uuid": {"uuid": "2/1"}},
+ {"device_id": {"device_uuid": {"uuid": "R6"}}, "endpoint_uuid": {"uuid": "2/2"}}
+ ]},
+ {"link_id": {"link_uuid": {"uuid": "R5==R7"}}, "link_endpoint_ids": [
+ {"device_id": {"device_uuid": {"uuid": "R5"}}, "endpoint_uuid": {"uuid": "2/3"}},
+ {"device_id": {"device_uuid": {"uuid": "R7"}}, "endpoint_uuid": {"uuid": "2/5"}}
+ ]},
+ {"link_id": {"link_uuid": {"uuid": "R6==R1"}}, "link_endpoint_ids": [
+ {"device_id": {"device_uuid": {"uuid": "R6"}}, "endpoint_uuid": {"uuid": "2/1"}},
+ {"device_id": {"device_uuid": {"uuid": "R1"}}, "endpoint_uuid": {"uuid": "2/2"}}
+ ]},
+ {"link_id": {"link_uuid": {"uuid": "R6==R5"}}, "link_endpoint_ids": [
+ {"device_id": {"device_uuid": {"uuid": "R6"}}, "endpoint_uuid": {"uuid": "2/2"}},
+ {"device_id": {"device_uuid": {"uuid": "R5"}}, "endpoint_uuid": {"uuid": "2/1"}}
+ ]},
+ {"link_id": {"link_uuid": {"uuid": "R7==R1"}}, "link_endpoint_ids": [
+ {"device_id": {"device_uuid": {"uuid": "R7"}}, "endpoint_uuid": {"uuid": "2/1"}},
+ {"device_id": {"device_uuid": {"uuid": "R1"}}, "endpoint_uuid": {"uuid": "2/3"}}
+ ]},
+ {"link_id": {"link_uuid": {"uuid": "R7==R3"}}, "link_endpoint_ids": [
+ {"device_id": {"device_uuid": {"uuid": "R7"}}, "endpoint_uuid": {"uuid": "2/3"}},
+ {"device_id": {"device_uuid": {"uuid": "R3"}}, "endpoint_uuid": {"uuid": "2/3"}}
+ ]},
+ {"link_id": {"link_uuid": {"uuid": "R7==R5"}}, "link_endpoint_ids": [
+ {"device_id": {"device_uuid": {"uuid": "R7"}}, "endpoint_uuid": {"uuid": "2/5"}},
+ {"device_id": {"device_uuid": {"uuid": "R5"}}, "endpoint_uuid": {"uuid": "2/3"}}
+ ]}
+ ],
+ "services": [
+ {
+ "service_id": {
+ "context_id": {"context_uuid": {"uuid": "admin"}}, "service_uuid": {"uuid": "test-svc"}
+ },
+ "service_type": 2,
+ "service_status": {"service_status": 1},
+ "service_endpoint_ids": [
+ {"device_id":{"device_uuid":{"uuid":"R1"}},"endpoint_uuid":{"uuid":"1/1"}},
+ {"device_id":{"device_uuid":{"uuid":"R4"}},"endpoint_uuid":{"uuid":"1/1"}}
+ ],
+ "service_constraints": [
+ {"sla_capacity": {"capacity_gbps": 10.0}},
+ {"sla_latency": {"e2e_latency_ms": 15.2}}
+ ],
+ "service_config": {"config_rules": [
+ {"action": 1, "custom": {"resource_key": "/settings", "resource_value": {
+ "address_families": ["IPV4"], "bgp_as": 65000, "bgp_route_target": "65000:123",
+ "mtu": 1512, "vlan_id": 111
+ }}},
+ {"action": 1, "custom": {"resource_key": "/device[R1]/endpoint[1/1]/settings", "resource_value": {
+ "sub_interface_index": 0, "vlan_id": 111
+ }}},
+ {"action": 1, "custom": {"resource_key": "/device[R4]/endpoint[1/1]/settings", "resource_value": {
+ "sub_interface_index": 0, "vlan_id": 111
+ }}}
+ ]}
+ }
+ ]
+}
diff --git a/src/service/tests/test_service_recompute_cons.sh b/src/service/tests/test_service_recompute_cons.sh
new file mode 100644
index 0000000000000000000000000000000000000000..e5bc18895b2968ba99b7262458ed988e57ee920c
--- /dev/null
+++ b/src/service/tests/test_service_recompute_cons.sh
@@ -0,0 +1,20 @@
+#!/bin/bash
+# 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.
+
+source my_deploy.sh
+./deploy/all.sh
+
+source tfs_runtime_env_vars.sh
+PYTHONPATH=./src pytest --log-level=INFO --verbose src/service/tests/test_unitary_recompute_conns.py
diff --git a/src/service/tests/test_unitary_recompute_conns.py b/src/service/tests/test_unitary_recompute_conns.py
new file mode 100644
index 0000000000000000000000000000000000000000..717e3af73b0d21d1dfeeab1e388c5df663417337
--- /dev/null
+++ b/src/service/tests/test_unitary_recompute_conns.py
@@ -0,0 +1,120 @@
+# 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, pytest
+from common.Constants import DEFAULT_CONTEXT_NAME
+from common.proto.context_pb2 import ContextId, Service
+from common.tools.descriptor.Loader import DescriptorLoader, check_descriptor_load_results, validate_empty_scenario
+from common.tools.grpc.Tools import grpc_message_to_json_string
+from common.tools.object_factory.Context import json_context_id
+from context.client.ContextClient import ContextClient
+from device.client.DeviceClient import DeviceClient
+from service.client.ServiceClient import ServiceClient
+
+LOGGER = logging.getLogger(__name__)
+LOGGER.setLevel(logging.DEBUG)
+
+DESCRIPTOR_FILE = 'src/service/tests/descriptors_recompute_conns.json'
+ADMIN_CONTEXT_ID = ContextId(**json_context_id(DEFAULT_CONTEXT_NAME))
+
+@pytest.fixture(scope='session')
+def context_client():
+ _client = ContextClient()
+ yield _client
+ _client.close()
+
+@pytest.fixture(scope='session')
+def device_client():
+ _client = DeviceClient()
+ yield _client
+ _client.close()
+
+@pytest.fixture(scope='session')
+def service_client():
+ _client = ServiceClient()
+ yield _client
+ _client.close()
+
+
+def test_service_recompute_connection(
+ context_client : ContextClient, # pylint: disable=redefined-outer-name
+ device_client : DeviceClient, # pylint: disable=redefined-outer-name
+ service_client : ServiceClient, # pylint: disable=redefined-outer-name
+) -> None:
+
+ # ===== Setup scenario =============================================================================================
+ validate_empty_scenario(context_client)
+
+ # Load descriptors and validate the base scenario
+ descriptor_loader = DescriptorLoader(
+ descriptors_file=DESCRIPTOR_FILE, context_client=context_client, device_client=device_client,
+ service_client=service_client)
+ results = descriptor_loader.process()
+ check_descriptor_load_results(results, descriptor_loader)
+ descriptor_loader.validate()
+
+
+ # ===== Recompute Connection =======================================================================================
+ response = context_client.ListServices(ADMIN_CONTEXT_ID)
+ LOGGER.info('Services[{:d}] = {:s}'.format(len(response.services), grpc_message_to_json_string(response)))
+ assert len(response.services) == 1
+ service = response.services[0]
+ service_id = service.service_id
+
+ response = context_client.ListConnections(service_id)
+ LOGGER.info(' ServiceId[{:s}] => Connections[{:d}] = {:s}'.format(
+ grpc_message_to_json_string(service_id), len(response.connections), grpc_message_to_json_string(response)))
+ assert len(response.connections) == 1 # 1 connection per service
+ str_old_connections = grpc_message_to_json_string(response)
+
+ # Change path first time
+ request = Service()
+ request.CopyFrom(service)
+ del request.service_endpoint_ids[:] # pylint: disable=no-member
+ del request.service_constraints[:] # pylint: disable=no-member
+ del request.service_config.config_rules[:] # pylint: disable=no-member
+ service_client.RecomputeConnections(request)
+
+ response = context_client.ListConnections(service_id)
+ LOGGER.info(' ServiceId[{:s}] => Connections[{:d}] = {:s}'.format(
+ grpc_message_to_json_string(service_id), len(response.connections), grpc_message_to_json_string(response)))
+ assert len(response.connections) == 1 # 1 connection per service
+ str_new_connections = grpc_message_to_json_string(response)
+
+ assert str_old_connections != str_new_connections
+
+ str_old_connections = str_new_connections
+
+ # Change path second time
+ request = Service()
+ request.CopyFrom(service)
+ del request.service_endpoint_ids[:] # pylint: disable=no-member
+ del request.service_constraints[:] # pylint: disable=no-member
+ del request.service_config.config_rules[:] # pylint: disable=no-member
+ service_client.RecomputeConnections(request)
+
+ response = context_client.ListConnections(service_id)
+ LOGGER.info(' ServiceId[{:s}] => Connections[{:d}] = {:s}'.format(
+ grpc_message_to_json_string(service_id), len(response.connections), grpc_message_to_json_string(response)))
+ assert len(response.connections) == 1 # 1 connection per service
+ str_new_connections = grpc_message_to_json_string(response)
+
+ assert str_old_connections != str_new_connections
+
+
+ # ===== Cleanup scenario ===========================================================================================
+ # Validate and unload the base scenario
+ descriptor_loader.validate()
+ descriptor_loader.unload()
+ validate_empty_scenario(context_client)
diff --git a/src/webui/service/templates/device/detail.html b/src/webui/service/templates/device/detail.html
index 1b4b43f5ad12956ae8bb2b1a843ce5e57ef29a2c..4d33578e2532c26b4062565bd2cbb52106773a1a 100644
--- a/src/webui/service/templates/device/detail.html
+++ b/src/webui/service/templates/device/detail.html
@@ -47,6 +47,7 @@
UUID: {{ device.device_id.device_uuid.uuid }}
Name: {{ device.name }}
Type: {{ device.device_type }}
+ Controller: {{ device.controller_id.device_uuid.uuid }}
Status: {{ dose.Name(device.device_operational_status).replace('DEVICEOPERATIONALSTATUS_', '') }}
Drivers: