diff --git a/scripts/show_logs_policy.sh b/scripts/show_logs_policy.sh new file mode 100755 index 0000000000000000000000000000000000000000..ee4fae277690b7aadd6eb42bda5791a28b86987c --- /dev/null +++ b/scripts/show_logs_policy.sh @@ -0,0 +1,27 @@ +#!/bin/bash +# Copyright 2022-2025 ETSI SDG TeraFlowSDN (TFS) (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. + +######################################################################################################################## +# Define your deployment settings here +######################################################################################################################## + +# If not already set, set the name of the Kubernetes namespace to deploy to. +export TFS_K8S_NAMESPACE=${TFS_K8S_NAMESPACE:-"tfs"} + +######################################################################################################################## +# Automated steps start here +######################################################################################################################## + +kubectl --namespace $TFS_K8S_NAMESPACE logs deployment/policyservice diff --git a/src/tests/automation/README.md b/src/tests/automation/README.md new file mode 100644 index 0000000000000000000000000000000000000000..c0b62f80ff55af915fbe544b256754edbc2f71db --- /dev/null +++ b/src/tests/automation/README.md @@ -0,0 +1,109 @@ +# Tests for the TFS Automation Service + +This test invokes a closed loop example using the TFS Automation service. +For Automation to have proper context, we use the p4-fabric-tna test to provision an example topology, forwarding service, and telemetry service. +On top of this setup, Automation on the one hand employs an Analyzer to ingest telemetry data stemming from the dataplane, process this data, and define a target KPI of interest, while on the other hand employs Policy to define what action needs to happen in the dataplane if an alarm is raised on this KPI. + +## Steps to setup and run a TFS program atop Stratum + +To conduct this test, follow the steps below. + +### Setup pyenv + +Setup the Python virtual environment as follows: + +```shell +python3 -m venv venv && source venv/bin/activate +``` + +### Deploy TFS + +Deploy TFS as follows: + +```shell +cd ~/tfs-ctrl/ +source my_deploy.sh && source tfs_runtime_env_vars.sh +./deploy/all.sh +``` + +### Path setup + +Ensure that `PATH` variable contains the parent project directory, e.g., "home/$USER/tfs-ctrl". + +Ensure that `PYTHONPATH` variable contains the source code directory of TFS, e.g., "home/$USER/tfs-ctrl/src" + +## Test + +### Provision test + +First deploy the TFS topology, connectivity, and telemetry services as follows: + +```shell +cd ~/tfs-ctrl/ +source my_deploy.sh && source tfs_runtime_env_vars.sh +bash src/tests/p4-fabric-tna/setup.sh +bash src/tests/p4-fabric-tna/run_test_01_bootstrap.sh +bash src/tests/p4-fabric-tna/run_test_03a_service_provision_l2.sh +bash src/tests/p4-fabric-tna/run_test_04a_service_provision_l3.sh +bash src/tests/p4-fabric-tna/run_test_06a_service_provision_int.sh +``` + +Once this is done, login on the WebUI to observe the example topology and verify that there are 2 services in ACTIVE state: + +- A P4 L2 forwarding that establishes connectivity between the hosts of the topology +- A P4 INT service that invokes the INT Telemetry Collector as a service + +``` +http:///webui +``` + +Then, login on Grafana to observe the `Latency` Dashboard already created for you. + +``` +http:///grafana +``` + +Finally, login on Prometheus to check the KPI ID of interest. + +``` +http://:30090/ +``` + +Example KPI of interest could be `KPISAMPLETYPE_INT_HOP_LAT`. + +#### Fill-in input JSON file for Automation + +Open the `automation.json` file located in `descriptors/`. and fill in the following critical fattrbutes: + +- Under `target_service_id.service_uuid.uuid`, add the service uuid of the L2 forwarding service that you saw on the WebUI (Service tab) +- Under `target_service_id.context_id.context_uuid.uuid`, add the context uuid that you saw on the WebUI (Service tab, open any service) +- Under `telemetry_service_id.service_uuid.uuid`, add the service uuid of the INT forwarding service that you saw on the WebUI (Service tab) +- Under `telemetry_service_id.context_id.context_uuid.uuid`, add the context uuid that you saw on the WebUI (same as above) +- Under `input_kpi_ids.kpi_id.uuid`, add the KPI ID you saw when you queried the `KPISAMPLETYPE_INT_HOP_LAT` KPI on Prometheus +- In case you want to tweak the KPI thresholds, modify `analyzer.parameters.thresholds` + +Now, you are ready to fire the test: + +```shell +cd ~/tfs-ctrl/ +bash +``` + +To observe the logs of Automation, do: + +```shell +cd ~/tfs-ctrl/ +bash src/tests/automation/run_test_automation.sh +``` + +### Deprovision test + +To deprovision the test, follow the steps below: + +```shell + +bash src/tests/p4-fabric-tna/run_test_03b_service_deprovision_l2.sh +bash src/tests/p4-fabric-tna/run_test_04b_service_deprovision_l3.sh +bash src/tests/p4-fabric-tna/run_test_06b_service_deprovision_int.sh +bash src/tests/p4-fabric-tna/run_test_07_cleanup.sh +``` diff --git a/src/tests/automation/__init__.py b/src/tests/automation/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..023830645e0fcb60e3f8583674a954810af222f2 --- /dev/null +++ b/src/tests/automation/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2022-2024 ETSI SDG TeraFlowSDN (TFS) (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. diff --git a/src/tests/automation/descriptors/automation.json b/src/tests/automation/descriptors/automation.json new file mode 100644 index 0000000000000000000000000000000000000000..5876de4ae5ea830a33f5797bd361dffa5946e134 --- /dev/null +++ b/src/tests/automation/descriptors/automation.json @@ -0,0 +1,38 @@ +{ + "target_service_id": { + "service_uuid": {"uuid": "66d498ad-5d94-5d90-8cb4-861e30689c64"}, + "context_id": {"context_uuid": {"uuid": "43813baf-195e-5da6-af20-b3d0922e71a7"}} + }, + "telemetry_service_id": { + "service_uuid": {"uuid": "db73d789-4abc-5514-88bb-e21f7e31d36a"}, + "context_id": {"context_uuid": {"uuid": "43813baf-195e-5da6-af20-b3d0922e71a7"}} + }, + "analyzer":{ + "operation_mode": "ANALYZEROPERATIONMODE_STREAMING", + "parameters": { + "thresholds": "{\"task_type\": \"AggregationHandler\",\"task_parameter\": [ {\"avg\": [0, 2500]}]}" + }, + "input_kpi_ids": [ + {"kpi_id": { "uuid": "b9f915e2-402d-4788-9e7d-6bd1055b5e8b"}} + ], + "output_kpi_ids": [ + {"kpi_id": { "uuid": "c45b09d8-c84a-45d8-b4c2-9fa9902d157d"}} + ], + "batch_min_duration_s": 10, + "batch_min_size": 5 + }, + "policy":{ + "serviceId": { + "context_id": {"context_uuid": {"uuid": "43813baf-195e-5da6-af20-b3d0922e71a7"}}, + "service_uuid": {"uuid": "66d498ad-5d94-5d90-8cb4-861e30689c64"} + }, + "policyRuleBasic": { + "actionList": [ + { + "action": "POLICY_RULE_ACTION_RECALCULATE_PATH", + "action_config": [] + } + ] + } + } +} diff --git a/src/tests/automation/run_test_automation.sh b/src/tests/automation/run_test_automation.sh new file mode 100644 index 0000000000000000000000000000000000000000..ce8938cae609b6272aac3a9752253f4ddbcc1115 --- /dev/null +++ b/src/tests/automation/run_test_automation.sh @@ -0,0 +1,17 @@ +#!/bin/bash +# Copyright 2022-2026 ETSI SDG TeraFlowSDN (TFS) (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 tfs_runtime_env_vars.sh +python3 -m pytest --verbose src/tests/automation/test_functional_automation.py diff --git a/src/tests/automation/test_functional_automation.py b/src/tests/automation/test_functional_automation.py new file mode 100644 index 0000000000000000000000000000000000000000..a29307765af7ef954a877ffdacb4cb79c1e468a4 --- /dev/null +++ b/src/tests/automation/test_functional_automation.py @@ -0,0 +1,202 @@ +# Copyright 2022-2026 ETSI SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import json +import logging +import uuid +from common.proto.analytics_frontend_pb2 import AnalyzerId, AnalyzerOperationMode +from common.proto.automation_pb2 import ZSMCreateRequest +from common.proto.context_pb2 import ContextId, ServiceId, ServiceStatusEnum, ServiceList +from common.proto.kpi_manager_pb2 import KpiId, KpiDescriptorList, KpiDescriptorFilter +from common.proto.policy_pb2 import PolicyRuleKpiId +from common.proto.policy_action_pb2 import PolicyRuleAction, PolicyRuleActionEnum +from common.tools.descriptor.Loader import DescriptorLoader, check_descriptor_load_results +from automation.client.AutomationClient import AutomationClient +from context.client.ContextClient import ContextClient +from device.client.DeviceClient import DeviceClient +from kpi_manager.client.KpiManagerClient import KpiManagerClient +from common.proto.policy_pb2 import PolicyRuleStateEnum +from service.client.ServiceClient import ServiceClient +from tests.Fixtures import context_client, device_client, \ + service_client, automation_client, kpi_manager_client # pylint: disable=unused-import +from tests.tools.test_tools_p4 import * + +LOGGER = logging.getLogger(__name__) +LOGGER.setLevel(logging.DEBUG) + +TEST_PATH = os.path.join( + os.path.dirname( + os.path.abspath(__file__) + ) + '/descriptors') +assert os.path.exists(TEST_PATH), "Invalid path to tests" + +DESC_FILE_SERVICE_AUTOMATION = os.path.join(TEST_PATH, 'automation.json') +assert os.path.exists(DESC_FILE_SERVICE_AUTOMATION),\ + "Invalid path to the automation descriptor" + +def test_service_zsm_create( + context_client : ContextClient, # pylint: disable=redefined-outer-name + device_client : DeviceClient, # pylint: disable=redefined-outer-name + service_client : ServiceClient, # pylint: disable=redefined-outer-name + automation_client : AutomationClient, # pylint: disable=redefined-outer-name + kpi_manager_client : KpiManagerClient +) -> None: + + # Load service + descriptor_loader = DescriptorLoader( + descriptors_file=DESC_FILE_SERVICE_AUTOMATION, + context_client=context_client, device_client=device_client, service_client=service_client + ) + results = descriptor_loader.process() + check_descriptor_load_results(results, descriptor_loader) + + data = None + with open(DESC_FILE_SERVICE_AUTOMATION) as f: + data = json.load(f) + + loaded_request : ZSMCreateRequest = ZSMCreateRequest(**data) # type: ignore + assert loaded_request + + # Mark important service IDs + context_uuid = loaded_request.target_service_id.context_id.context_uuid.uuid + target_service_uuid = loaded_request.target_service_id.service_uuid.uuid + telemetry_service_uuid = loaded_request.telemetry_service_id.service_uuid.uuid + + # Check that the associated services are valid + try: + _check_context(context_client, context_uuid, target_service_uuid, telemetry_service_uuid) + except Exception as ex: + raise(ex) + + # Add important information in the request + loaded_request = _zsm_create_request(loaded_request, kpi_manager_client) + # loaded_request = _static_zsm_create_request() + + # Invoke Automation + automation_client.ZSMCreate(loaded_request) + +def _check_context(context_client, context_uuid : str, target_service_uuid : str, telemetry_service_uuid : str): + context_id : ContextId = ContextId() # type: ignore + context_id.context_uuid.uuid = context_uuid + + # Get the available services + service_list : ServiceList = context_client.ListServices(context_id) # type: ignore + for service in service_list.services: + service_id = service.service_id + assert service_id + + ctx_uuid = service_id.context_id.context_uuid.uuid + assert ctx_uuid == context_uuid, "Context UUID does not match" + + service_uuid = service_id.service_uuid.uuid + assert service_uuid, "Invalid service UUID" + + if service_uuid not in [target_service_uuid, telemetry_service_uuid]: + continue + + # The service we care about must be active + assert service.service_status.service_status == ServiceStatusEnum.SERVICESTATUS_ACTIVE + +def _zsm_create_request(request : ZSMCreateRequest, kpi_manager_client : KpiManagerClient) -> ZSMCreateRequest: # type: ignore + LOGGER.info("Preparing the ZSM request") + telemetry_service_obj = ServiceId() + telemetry_service_obj.service_uuid.uuid = request.telemetry_service_id.service_uuid.uuid + + ### Analyzer + # Analyzer requires an ID + if not request.analyzer.HasField("analyzer_id"): + request.analyzer.analyzer_id.analyzer_id.uuid = str(uuid.uuid4()) + + # Validate the inserted KPIs + for kpi_id in request.analyzer.input_kpi_ids: + create_kpi_filter_request = KpiDescriptorFilter() + create_kpi_filter_request.kpi_id.append(kpi_id) + create_kpi_filter_request.service_id.append(telemetry_service_obj) + desc_list = kpi_manager_client.SelectKpiDescriptor(create_kpi_filter_request) + assert isinstance(desc_list, KpiDescriptorList) + for kpi in desc_list.kpi_descriptor_list: + LOGGER.info(f"Validated KPI:\n{kpi}") + + ### Policy + assert request.policy.policyRuleBasic, "Policy must at least have a basic rule body" + # Policy requires an ID + if not request.policy.policyRuleBasic.HasField("policyRuleId"): + request.policy.policyRuleBasic.policyRuleId.uuid.uuid = str(uuid.uuid4()) + # Policy requires a state + if not request.policy.policyRuleBasic.HasField("policyRuleState"): + request.policy.policyRuleBasic.policyRuleState.policyRuleState = PolicyRuleStateEnum.POLICY_UNDEFINED + request.policy.policyRuleBasic.policyRuleState.policyRuleStateMessage = "About to insert policy" + # Link policy with the output KPI + for kpi_id in request.analyzer.output_kpi_ids: + rule_kpi = PolicyRuleKpiId() + rule_kpi.policyRuleKpiUuid.uuid = kpi_id.kpi_id.uuid + request.policy.policyRuleBasic.policyRuleKpiList.append(rule_kpi) + + # Policy requires a priority + request.policy.policyRuleBasic.policyRulePriority = 1 + + return request + +def _static_zsm_create_request(): + context_uuid = "43813baf-195e-5da6-af20-b3d0922e71a7" + target_service_uuid = "66d498ad-5d94-5d90-8cb4-861e30689c64" + telemetry_service_uuid = "db73d789-4abc-5514-88bb-e21f7e31d36a" + kpi_input_uuid = "b7006457-610b-4d76-b3fe-7ef36f1d4f49" + kpi_output_uuid = "c45b09d8-c84a-45d8-b4c2-9fa9902d157d" + + request : ZSMCreateRequest = ZSMCreateRequest() # type: ignore + + # Services + request.target_service_id.service_uuid.uuid = target_service_uuid + request.target_service_id.context_id.context_uuid.uuid = context_uuid + request.telemetry_service_id.service_uuid.uuid = telemetry_service_uuid + request.telemetry_service_id.context_id.context_uuid.uuid = context_uuid + + # Analyzer + request.analyzer.analyzer_id.analyzer_id.uuid = str(uuid.uuid4()) + request.analyzer.operation_mode = AnalyzerOperationMode.ANALYZEROPERATIONMODE_STREAMING + threshold_dict = { + "task_type": "AggregationHandler", + "task_parameter": [ + {"avg": [0, 2500]} + ] + } + request.analyzer.parameters["thresholds"] = json.dumps(threshold_dict) + request.analyzer.parameters["window_size"] = "10" + request.analyzer.parameters["window_slider"] = "5" + request.analyzer.batch_min_duration_s = 10 + request.analyzer.batch_min_size = 5 + + i_kpi_id = KpiId() + i_kpi_id.kpi_id.uuid = kpi_input_uuid + request.analyzer.input_kpi_ids.append(i_kpi_id) + + o_kpi_id = KpiId() + o_kpi_id.kpi_id.uuid = kpi_output_uuid + request.analyzer.output_kpi_ids.append(o_kpi_id) + + # Policy + action = PolicyRuleAction() + action.action = PolicyRuleActionEnum.POLICY_RULE_ACTION_CALL_SERVICE_RPC + request.policy.policyRuleBasic.policyRuleId.uuid.uuid = str(uuid.uuid4()) + request.policy.policyRuleBasic.actionList.append(action) + request.policy.policyRuleBasic.policyRuleState.policyRuleState = PolicyRuleStateEnum.POLICY_UNDEFINED + request.policy.policyRuleBasic.policyRuleState.policyRuleStateMessage = "About to insert policy" + rule_kpi = PolicyRuleKpiId() + rule_kpi.policyRuleKpiUuid.uuid = o_kpi_id.kpi_id.uuid + request.policy.policyRuleBasic.policyRuleKpiList.append(rule_kpi) + request.policy.serviceId.context_id.context_uuid.uuid = context_uuid + request.policy.serviceId.service_uuid.uuid = target_service_uuid + + return request