diff --git a/src/device/service/drivers/gnmi_openconfig/handlers/Acl.py b/src/device/service/drivers/gnmi_openconfig/handlers/Acl.py index 06061a0b82f3a9c2f0c123a5350d68b0fa663457..0e6f31ff73da12ee91ae06deeab54db5715f7602 100644 --- a/src/device/service/drivers/gnmi_openconfig/handlers/Acl.py +++ b/src/device/service/drivers/gnmi_openconfig/handlers/Acl.py @@ -30,7 +30,8 @@ _TFS_OC_RULE_TYPE = { _TFS_OC_FWD_ACTION = { 'ACLFORWARDINGACTION_DROP': 'DROP', 'ACLFORWARDINGACTION_ACCEPT': 'ACCEPT', - 'ACLFORWARDINGACTION_REJECT': 'REJECT', + #'ACLFORWARDINGACTION_REJECT': 'REJECT', # Correct according to OpenConfig. + 'ACLFORWARDINGACTION_REJECT': 'DROP', # - Arista EOS only supports ACCEPT/DROP } _OC_TFS_RULE_TYPE = {v: k for k, v in _TFS_OC_RULE_TYPE.items()} @@ -89,7 +90,7 @@ class AclHandler(_Handler): y_entries = y_set.create_path('acl-entries') for entry in rs.get('entries', []): seq = int(entry['sequence_id']) - m_ = entry['match'] + m_ = entry.get('match', dict()) src_address = m_.get('src_address', '0.0.0.0/0') dst_address = m_.get('dst_address', '0.0.0.0/0') src_port = m_.get('src_port') @@ -110,10 +111,9 @@ class AclHandler(_Handler): if src_port or dst_port: y_trans = y_e.create_path('transport') if src_port: - y_trans.create_path("config/source-port", int(src_port)) + y_trans.create_path('config/source-port', int(src_port)) if dst_port: - y_trans.create_path("config/destination-port", int(dst_port)) - y_ipv4.create_path('config/protocol', int(proto)) + y_trans.create_path('config/destination-port', int(dst_port)) y_act = y_e.create_path('actions') y_act.create_path('config/forwarding-action', act) @@ -183,14 +183,24 @@ class AclHandler(_Handler): act = ace.get('actions', {}).get('config', {}).get('forwarding-action', 'DROP') fwd_tfs = _OC_TFS_FWD_ACTION[act] ipv4_cfg = ace.get('ipv4', {}).get('config', {}) + transport_cfg = ace.get('transport', {}).get('config', {}) + + match_conditions = dict() + if 'source-address' in ipv4_cfg: + match_conditions['src_address'] = ipv4_cfg['source-address'] + if 'destination-address' in ipv4_cfg: + match_conditions['dst_address'] = ipv4_cfg['destination-address'] + if 'protocol' in ipv4_cfg: + match_conditions['protocol'] = ipv4_cfg['protocol'] + if 'source-port' in transport_cfg: + match_conditions['src_port'] = transport_cfg['source-port'] + if 'destination-port' in transport_cfg: + match_conditions['dst_port'] = transport_cfg['destination-port'] rule_set['entries'].append( { 'sequence_id': seq, - 'match': { - 'src_address': ipv4_cfg.get('source-address', ''), - 'dst_address': ipv4_cfg.get('destination-address', ''), - }, + 'match': match_conditions, 'action': {'forward_action': fwd_tfs}, } ) diff --git a/src/nbi/service/ietf_acl/Acl.py b/src/nbi/service/ietf_acl/Acl.py index c0635968bacfa7c6168f845ca9347aa1b63ee25e..d48b645de9fd68e136724697ea82a6fecf0b5346 100644 --- a/src/nbi/service/ietf_acl/Acl.py +++ b/src/nbi/service/ietf_acl/Acl.py @@ -13,6 +13,7 @@ # limitations under the License. import json, logging, re +from flask import jsonify from flask_restful import Resource from werkzeug.exceptions import NotFound from common.proto.context_pb2 import ConfigActionEnum, ConfigRule @@ -20,6 +21,7 @@ from common.tools.context_queries.Device import get_device from context.client.ContextClient import ContextClient from device.client.DeviceClient import DeviceClient from nbi.service._tools.Authentication import HTTP_AUTH +from nbi.service._tools.HttpStatusCodes import HTTP_NOCONTENT from .ietf_acl_parser import ietf_acl_from_config_rule_resource_value LOGGER = logging.getLogger(__name__) @@ -72,4 +74,7 @@ class Acl(Resource): del device.device_config.config_rules[:] device.device_config.config_rules.extend(delete_config_rules) device_client.ConfigureDevice(device) - return None + + response = jsonify({}) + response.status_code = HTTP_NOCONTENT + return response diff --git a/src/nbi/service/ietf_acl/Acls.py b/src/nbi/service/ietf_acl/Acls.py index e966f2c0c0fda057dfdf3c5d397c10b667f69d40..e9268226b8c7ca03a90864118462aaf0025112ca 100644 --- a/src/nbi/service/ietf_acl/Acls.py +++ b/src/nbi/service/ietf_acl/Acls.py @@ -23,6 +23,7 @@ from common.tools.grpc.Tools import grpc_message_to_json_string from context.client.ContextClient import ContextClient from device.client.DeviceClient import DeviceClient from nbi.service._tools.Authentication import HTTP_AUTH +from nbi.service._tools.HttpStatusCodes import HTTP_CREATED from .ietf_acl_parser import AclDirectionEnum, config_rule_from_ietf_acl from .YangValidator import YangValidator @@ -129,4 +130,7 @@ class Acls(Resource): device_client = DeviceClient() device_client.ConfigureDevice(device) - return jsonify({}) + + response = jsonify({}) + response.status_code = HTTP_CREATED + return response diff --git a/src/nbi/service/ietf_acl/ietf_acl_parser.py b/src/nbi/service/ietf_acl/ietf_acl_parser.py index 7305775b8f83392afc9902e350729073ada1a6fd..66c041f200dd6582e6f879016ccc63fd573151b7 100644 --- a/src/nbi/service/ietf_acl/ietf_acl_parser.py +++ b/src/nbi/service/ietf_acl/ietf_acl_parser.py @@ -28,9 +28,12 @@ class AclDirectionEnum(Enum): class Ipv4(BaseModel): dscp: int = 0 - source_ipv4_network: str = Field(serialization_alias='source-ipv4-network', default='') - destination_ipv4_network: str = Field( - serialization_alias='destination-ipv4-network', default='' + protocol: int = 0 + source_ipv4_network: Optional[str] = Field( + serialization_alias='source-ipv4-network', default=None + ) + destination_ipv4_network: Optional[str] = Field( + serialization_alias='destination-ipv4-network', default=None ) @@ -45,9 +48,15 @@ class Tcp(BaseModel): destination_port: Optional[Port] = Field(serialization_alias='destination-port', default=None) +class Udp(BaseModel): + source_port: Optional[Port] = Field(serialization_alias='source-port', default=None) + destination_port: Optional[Port] = Field(serialization_alias='destination-port', default=None) + + class Matches(BaseModel): ipv4: Ipv4 = Ipv4() tcp: Optional[Tcp] = None + udp: Optional[Udp] = None class Action(BaseModel): @@ -237,27 +246,42 @@ def config_rule_from_ietf_acl( def ietf_acl_from_config_rule_resource_value(config_rule_rv: Dict) -> Dict: - rule_set = config_rule_rv['rule_set'] + rule_set = config_rule_rv.get('rule_set', dict()) ace = [] - for acl_entry in rule_set['entries']: - match_ = acl_entry['match'] + for acl_entry in rule_set.get('entries', list()): + match_ = acl_entry.get('match', dict()) + protocol = match_.get('protocol', 0) ipv4 = Ipv4( - dscp=match_['dscp'], - source_ipv4_network=match_['src_address'], - destination_ipv4_network=match_['dst_address'], + dscp=match_.get('dscp', 0), + protocol=protocol, + source_ipv4_network=match_.get('src_address'), + destination_ipv4_network=match_.get('dst_address'), ) + + src_port = match_.get('src_port') + src_port = None if src_port is None else Port(port=src_port) + dst_port = match_.get('dst_port') + dst_port = None if dst_port is None else Port(port=dst_port) + tcp = None - if match_['tcp_flags']: + udp = None + if protocol == 6: tcp = Tcp( - flags=match_['tcp_flags'], - source_port=Port(port=match_['src_port']), - destination_port=Port(port=match_['dst_port']), + flags=match_.get('tcp_flags', 0), + source_port=src_port, + destination_port=dst_port, ) - matches = Matches(ipv4=ipv4, tcp=tcp) + elif protocol == 17: + udp = Udp( + source_port=src_port, + destination_port=dst_port, + ) + + matches = Matches(ipv4=ipv4, tcp=tcp, udp=udp) ace.append( Ace( - name=acl_entry['description'], + name=acl_entry.get('description', ''), matches=matches, actions=Action( forwarding=TFS_IETF_FORWARDING_ACTION_MAPPING[ diff --git a/src/tests/.gitlab-ci.yml b/src/tests/.gitlab-ci.yml index 9b256f1ae6f9a3d63996266e961f9409c8453d3b..723c26d6fff3b714edf27c937087a79a6f7555ba 100644 --- a/src/tests/.gitlab-ci.yml +++ b/src/tests/.gitlab-ci.yml @@ -26,6 +26,7 @@ include: #- local: '/src/tests/ofc25/.gitlab-ci.yml' #- local: '/src/tests/ryu-openflow/.gitlab-ci.yml' - local: '/src/tests/qkd_end2end/.gitlab-ci.yml' + - local: '/src/tests/acl_end2end/.gitlab-ci.yml' - local: '/src/tests/tools/mock_tfs_nbi_dependencies/.gitlab-ci.yml' - local: '/src/tests/tools/mock_qkd_node/.gitlab-ci.yml' diff --git a/src/tests/acl_end2end/.gitignore b/src/tests/acl_end2end/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..a47dc9eff49108f99e8869a9ea8981c1d6b321c1 --- /dev/null +++ b/src/tests/acl_end2end/.gitignore @@ -0,0 +1,19 @@ +# 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. + +clab-*/ +images/ +*.clab.yml.bak +*.tar +*.tar.gz diff --git a/src/tests/acl_end2end/.gitlab-ci.yml b/src/tests/acl_end2end/.gitlab-ci.yml new file mode 100644 index 0000000000000000000000000000000000000000..675dc70682ee12e3385eb5e50635d5b91ac07585 --- /dev/null +++ b/src/tests/acl_end2end/.gitlab-ci.yml @@ -0,0 +1,291 @@ +# 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. + +# Build, tag, and push the Docker image to the GitLab Docker registry +build acl_end2end: + variables: + TEST_NAME: 'acl_end2end' + stage: build + before_script: + - docker image prune --force + - docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY + script: + - docker buildx build -t "${TEST_NAME}:latest" -f ./src/tests/${TEST_NAME}/Dockerfile . + - docker tag "${TEST_NAME}:latest" "$CI_REGISTRY_IMAGE/${TEST_NAME}:latest" + - docker push "$CI_REGISTRY_IMAGE/${TEST_NAME}:latest" + after_script: + - docker image prune --force + rules: + - if: '$CI_PIPELINE_SOURCE == "merge_request_event" && ($CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "develop" || $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == $CI_DEFAULT_BRANCH)' + - if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "develop"' + - changes: + - src/common/**/*.py + - proto/*.proto + - src/tests/${TEST_NAME}/**/*.{py,in,sh,yml} + - src/tests/${TEST_NAME}/Dockerfile + - .gitlab-ci.yml + +# Deploy TeraFlowSDN and Execute end-2-end test +end2end_test acl_end2end: + timeout: 45m + variables: + TEST_NAME: 'acl_end2end' + stage: end2end_test + # Disable to force running it after all other tasks + #needs: + # - build acl_end2end + before_script: + # Cleanup old ContainerLab scenarios + - containerlab destroy --all --cleanup || true + + # Do Docker cleanup + - docker ps --all --quiet | xargs --no-run-if-empty docker stop + - docker container prune --force + - docker ps --all --quiet | xargs --no-run-if-empty docker rm --force + - docker image prune --force + - docker network prune --force + - docker volume prune --all --force + - docker buildx prune --force + + # Check MicroK8s is ready + - microk8s status --wait-ready + - LOOP_MAX_ATTEMPTS=10 + - LOOP_COUNTER=0 + - > + while ! kubectl get pods --all-namespaces &> /dev/null; do + printf "%c" "." + sleep 1 + LOOP_COUNTER=$((LOOP_COUNTER + 1)) + if [ "$LOOP_COUNTER" -ge "$LOOP_MAX_ATTEMPTS" ]; then + echo "Max attempts reached, exiting the loop." + exit 1 + fi + done + - kubectl get pods --all-namespaces + + # Always delete Kubernetes namespaces + - export K8S_NAMESPACES=$(kubectl get namespace -o jsonpath='{.items[*].metadata.name}') + - echo "K8S_NAMESPACES=${K8S_NAMESPACES}" + + - export OLD_NATS_NAMESPACES=$(echo "${K8S_NAMESPACES}" | tr ' ' '\n' | grep -E '^nats') + - echo "OLD_NATS_NAMESPACES=${OLD_NATS_NAMESPACES}" + - > + for ns in ${OLD_NATS_NAMESPACES}; do + if [[ "$ns" == nats* ]]; then + if helm3 status "$ns" &>/dev/null; then + helm3 uninstall "$ns" -n "$ns" + else + echo "Release '$ns' not found, skipping..." + fi + fi + done + - export OLD_NAMESPACES=$(echo "${K8S_NAMESPACES}" | tr ' ' '\n' | grep -E '^(tfs|crdb|qdb|kafka|nats)') + - echo "OLD_NAMESPACES=${OLD_NAMESPACES}" + - kubectl delete namespace ${OLD_NAMESPACES} || true + + # Clean-up Kubernetes Failed pods + - > + kubectl get pods --all-namespaces --no-headers --field-selector=status.phase=Failed + -o custom-columns=NAMESPACE:.metadata.namespace,NAME:.metadata.name | + xargs --no-run-if-empty --max-args=2 kubectl delete pod --namespace + + # Login Docker repository + - docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY + + script: + # Download Docker image to run the test + - docker pull "${CI_REGISTRY_IMAGE}/${TEST_NAME}:latest" + + # Check MicroK8s is ready + - microk8s status --wait-ready + - LOOP_MAX_ATTEMPTS=10 + - LOOP_COUNTER=0 + - > + while ! kubectl get pods --all-namespaces &> /dev/null; do + printf "%c" "." + sleep 1 + LOOP_COUNTER=$((LOOP_COUNTER + 1)) + if [ "$LOOP_COUNTER" -ge "$LOOP_MAX_ATTEMPTS" ]; then + echo "Max attempts reached, exiting the loop." + exit 1 + fi + done + - kubectl get pods --all-namespaces + + # Deploy ContainerLab Scenario + - RUNNER_PATH=`pwd` + #- cd $PWD/src/tests/${TEST_NAME} + - mkdir -p /tmp/clab/${TEST_NAME} + - cp -R src/tests/${TEST_NAME}/clab/* /tmp/clab/${TEST_NAME} + - tree -la /tmp/clab/${TEST_NAME} + - cd /tmp/clab/${TEST_NAME} + - containerlab deploy --reconfigure --topo acl_end2end.clab.yml + - cd $RUNNER_PATH + + # Wait for initialization of Device NOSes + - sleep 3 + - docker ps -a + + # Dump configuration of the firewall (before any configuration) + - containerlab exec --name acl_end2end --label clab-node-name=firewall --cmd "Cli --command \"enable"$'\n'$"show running-config\"" + + # Configure TeraFlowSDN deployment + # Uncomment if DEBUG log level is needed for the components + #- yq -i '((select(.kind=="Deployment").spec.template.spec.containers.[] | select(.name=="server").env.[]) | select(.name=="LOG_LEVEL").value) |= "DEBUG"' manifests/contextservice.yaml + #- yq -i '((select(.kind=="Deployment").spec.template.spec.containers.[] | select(.name=="server").env.[]) | select(.name=="LOG_LEVEL").value) |= "DEBUG"' manifests/deviceservice.yaml + #- yq -i '((select(.kind=="Deployment").spec.template.spec.containers.[] | select(.name=="frontend").env.[]) | select(.name=="LOG_LEVEL").value) |= "DEBUG"' manifests/pathcompservice.yaml + #- yq -i '((select(.kind=="Deployment").spec.template.spec.containers.[] | select(.name=="server").env.[]) | select(.name=="LOG_LEVEL").value) |= "DEBUG"' manifests/serviceservice.yaml + #- yq -i '((select(.kind=="Deployment").spec.template.spec.containers.[] | select(.name=="server").env.[]) | select(.name=="LOG_LEVEL").value) |= "DEBUG"' manifests/nbiservice.yaml + + - source src/tests/${TEST_NAME}/deploy_specs.sh + #- export TFS_REGISTRY_IMAGES="${CI_REGISTRY_IMAGE}" + #- export TFS_SKIP_BUILD="YES" + #- export TFS_IMAGE_TAG="latest" + #- echo "TFS_REGISTRY_IMAGES=${CI_REGISTRY_IMAGE}" + + # Deploy TeraFlowSDN + - ./deploy/crdb.sh + - ./deploy/nats.sh + - ./deploy/kafka.sh + #- ./deploy/qdb.sh + - ./deploy/tfs.sh + - ./deploy/show.sh + + ## Wait for Context to be subscribed to NATS + ## WARNING: this loop is infinite if there is no subscriber (such as monitoring). + ## Investigate if we can use a counter to limit the number of iterations. + ## For now, keep it commented out. + #- LOOP_MAX_ATTEMPTS=180 + #- LOOP_COUNTER=0 + #- > + # while ! kubectl --namespace $TFS_K8S_NAMESPACE logs deployment/contextservice -c server 2>&1 | grep -q 'Subscriber is Ready? True'; do + # echo "Attempt: $LOOP_COUNTER" + # kubectl --namespace $TFS_K8S_NAMESPACE logs deployment/contextservice -c server 2>&1; + # sleep 1; + # LOOP_COUNTER=$((LOOP_COUNTER + 1)) + # if [ "$LOOP_COUNTER" -ge "$LOOP_MAX_ATTEMPTS" ]; then + # echo "Max attempts reached, exiting the loop." + # break + # fi + # done + - kubectl --namespace $TFS_K8S_NAMESPACE logs deployment/contextservice -c server + + # Run end-to-end test: onboard scenario + - > + docker run -t --rm --name ${TEST_NAME} --network=host + --volume "$PWD/tfs_runtime_env_vars.sh:/var/teraflow/tfs_runtime_env_vars.sh" + --volume "$PWD/src/tests/${TEST_NAME}:/opt/results" + $CI_REGISTRY_IMAGE/${TEST_NAME}:latest /var/teraflow/run-onboarding.sh + + # Run end-to-end test: test connectivity with ping before creating ACL + - export TEST_CLIENT1_FW=$(containerlab exec --name acl_end2end --label clab-node-name=client1 --cmd 'ping -n -c3 172.16.11.1' --format json) + - echo $TEST_CLIENT1_FW + - echo $TEST_CLIENT1_FW | grep -E '3 packets transmitted, 3 received, 0\% packet loss' + - export TEST_CLIENT1_DC=$(containerlab exec --name acl_end2end --label clab-node-name=client1 --cmd 'ping -n -c3 172.16.10.10' --format json) + - echo $TEST_CLIENT1_DC + - echo $TEST_CLIENT1_DC | grep -E '3 packets transmitted, 3 received, 0\% packet loss' + + - export TEST_CLIENT2_FW=$(containerlab exec --name acl_end2end --label clab-node-name=client2 --cmd 'ping -n -c3 172.16.12.1' --format json) + - echo $TEST_CLIENT2_FW + - echo $TEST_CLIENT2_FW | grep -E '3 packets transmitted, 3 received, 0\% packet loss' + - export TEST_CLIENT2_DC=$(containerlab exec --name acl_end2end --label clab-node-name=client2 --cmd 'ping -n -c3 172.16.10.10' --format json) + - echo $TEST_CLIENT2_DC + - echo $TEST_CLIENT2_DC | grep -E '3 packets transmitted, 3 received, 0\% packet loss' + + # Run end-to-end test: create ACLs + - > + docker run -t --rm --name ${TEST_NAME} --network=host + --volume "$PWD/tfs_runtime_env_vars.sh:/var/teraflow/tfs_runtime_env_vars.sh" + --volume "$PWD/src/tests/${TEST_NAME}:/opt/results" + $CI_REGISTRY_IMAGE/${TEST_NAME}:latest /var/teraflow/run-acl-create.sh + + # Run end-to-end test: test connectivity with ping after creating ACL + - export TEST_CLIENT1_FW=$(containerlab exec --name acl_end2end --label clab-node-name=client1 --cmd 'ping -n -c3 172.16.11.1' --format json) + - echo $TEST_CLIENT1_FW + - echo $TEST_CLIENT1_FW | grep -E '3 packets transmitted, 3 received, 0\% packet loss' + - export TEST_CLIENT1_DC=$(containerlab exec --name acl_end2end --label clab-node-name=client1 --cmd 'ping -n -c3 172.16.10.10' --format json) + - echo $TEST_CLIENT1_DC + - echo $TEST_CLIENT1_DC | grep -E '3 packets transmitted, 0 received, 100\% packet loss' + + - export TEST_CLIENT2_FW=$(containerlab exec --name acl_end2end --label clab-node-name=client2 --cmd 'ping -n -c3 172.16.12.1' --format json) + - echo $TEST_CLIENT2_FW + - echo $TEST_CLIENT2_FW | grep -E '3 packets transmitted, 3 received, 0\% packet loss' + - export TEST_CLIENT2_DC=$(containerlab exec --name acl_end2end --label clab-node-name=client2 --cmd 'ping -n -c3 172.16.10.10' --format json) + - echo $TEST_CLIENT2_DC + - echo $TEST_CLIENT2_DC | grep -E '3 packets transmitted, 3 received, 0\% packet loss' + + # Run end-to-end test: delete ACLs + - > + docker run -t --rm --name ${TEST_NAME} --network=host + --volume "$PWD/tfs_runtime_env_vars.sh:/var/teraflow/tfs_runtime_env_vars.sh" + --volume "$PWD/src/tests/${TEST_NAME}:/opt/results" + $CI_REGISTRY_IMAGE/${TEST_NAME}:latest /var/teraflow/run-acl-delete.sh + + # Run end-to-end test: test connectivity with ping after deleting ACL + - export TEST_CLIENT1_FW=$(containerlab exec --name acl_end2end --label clab-node-name=client1 --cmd 'ping -n -c3 172.16.11.1' --format json) + - echo $TEST_CLIENT1_FW + - echo $TEST_CLIENT1_FW | grep -E '3 packets transmitted, 3 received, 0\% packet loss' + - export TEST_CLIENT1_DC=$(containerlab exec --name acl_end2end --label clab-node-name=client1 --cmd 'ping -n -c3 172.16.10.10' --format json) + - echo $TEST_CLIENT1_DC + - echo $TEST_CLIENT1_DC | grep -E '3 packets transmitted, 3 received, 0\% packet loss' + + - export TEST_CLIENT2_FW=$(containerlab exec --name acl_end2end --label clab-node-name=client2 --cmd 'ping -n -c3 172.16.12.1' --format json) + - echo $TEST_CLIENT2_FW + - echo $TEST_CLIENT2_FW | grep -E '3 packets transmitted, 3 received, 0\% packet loss' + - export TEST_CLIENT2_DC=$(containerlab exec --name acl_end2end --label clab-node-name=client2 --cmd 'ping -n -c3 172.16.10.10' --format json) + - echo $TEST_CLIENT2_DC + - echo $TEST_CLIENT2_DC | grep -E '3 packets transmitted, 3 received, 0\% packet loss' + + # Run end-to-end test: cleanup scenario + - > + docker run -t --rm --name ${TEST_NAME} --network=host + --volume "$PWD/tfs_runtime_env_vars.sh:/var/teraflow/tfs_runtime_env_vars.sh" + --volume "$PWD/src/tests/${TEST_NAME}:/opt/results" + $CI_REGISTRY_IMAGE/${TEST_NAME}:latest /var/teraflow/run-cleanup.sh + + after_script: + # Dump configuration of the firewall (on after_script) + - containerlab exec --name acl_end2end --label clab-node-name=firewall --cmd "Cli --command \"enable"$'\n'$"show running-config\"" + + # Dump TeraFlowSDN component logs + - source src/tests/${TEST_NAME}/deploy_specs.sh + - kubectl --namespace $TFS_K8S_NAMESPACE logs deployment/contextservice -c server + - kubectl --namespace $TFS_K8S_NAMESPACE logs deployment/deviceservice -c server + - kubectl --namespace $TFS_K8S_NAMESPACE logs deployment/pathcompservice -c frontend + - kubectl --namespace $TFS_K8S_NAMESPACE logs deployment/serviceservice -c server + - kubectl --namespace $TFS_K8S_NAMESPACE logs deployment/nbiservice -c server + + # Clean up + - RUNNER_PATH=`pwd` + #- cd $PWD/src/tests/${TEST_NAME} + - cd /tmp/clab/${TEST_NAME} + - containerlab destroy --topo acl_end2end.clab.yml --cleanup || true + - sudo rm -rf clab-acl_end2end/ .acl_end2end.clab.yml.bak || true + - cd $RUNNER_PATH + - kubectl delete namespaces tfs || true + - docker ps --all --quiet | xargs --no-run-if-empty docker stop + - docker container prune --force + - docker ps --all --quiet | xargs --no-run-if-empty docker rm --force + - docker network prune --force + - docker volume prune --all --force + - docker image prune --force + + #coverage: '/TOTAL\s+\d+\s+\d+\s+(\d+%)/' + rules: + - if: '$CI_PIPELINE_SOURCE == "merge_request_event" && ($CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "develop" || $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == $CI_DEFAULT_BRANCH)' + - if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "develop"' + artifacts: + when: always + reports: + junit: ./src/tests/${TEST_NAME}/report_*.xml diff --git a/src/tests/acl_end2end/Dockerfile b/src/tests/acl_end2end/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..3e3828d0b5c246388bda21c1008b7dc7c5363244 --- /dev/null +++ b/src/tests/acl_end2end/Dockerfile @@ -0,0 +1,86 @@ +# 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. + +FROM python:3.9-slim + +# Install dependencies +RUN apt-get --yes --quiet --quiet update && \ + apt-get --yes --quiet --quiet install wget g++ git && \ + rm -rf /var/lib/apt/lists/* + +# Set Python to show logs as they occur +ENV PYTHONUNBUFFERED=0 + +# Get generic Python packages +RUN python3 -m pip install --upgrade 'pip==25.2' +RUN python3 -m pip install --upgrade 'setuptools==79.0.0' 'wheel==0.45.1' +RUN python3 -m pip install --upgrade 'pip-tools==7.3.0' + +# Get common Python packages +# Note: this step enables sharing the previous Docker build steps among all the Python components +WORKDIR /var/teraflow +COPY common_requirements.in common_requirements.in +RUN pip-compile --quiet --output-file=common_requirements.txt common_requirements.in +RUN python3 -m pip install -r common_requirements.txt + +# Add common files into working directory +WORKDIR /var/teraflow/common +COPY src/common/. ./ +RUN rm -rf proto + +# Create proto sub-folder, copy .proto files, and generate Python code +RUN mkdir -p /var/teraflow/common/proto +WORKDIR /var/teraflow/common/proto +RUN touch __init__.py +COPY proto/*.proto ./ +RUN python3 -m grpc_tools.protoc -I=. --python_out=. --grpc_python_out=. *.proto +RUN rm *.proto +RUN find . -type f -exec sed -i -E 's/^(import\ .*)_pb2/from . \1_pb2/g' {} \; + +# Create component sub-folders, get specific Python packages +RUN mkdir -p /var/teraflow/tests/acl_end2end +WORKDIR /var/teraflow/tests/acl_end2end +COPY src/tests/acl_end2end/requirements.in requirements.in +RUN pip-compile --quiet --output-file=requirements.txt requirements.in +RUN python3 -m pip install -r requirements.txt + +# Add component files into working directory +WORKDIR /var/teraflow +COPY src/__init__.py ./__init__.py +COPY src/common/*.py ./common/ +COPY src/common/tests/. ./common/tests/ +COPY src/common/tools/. ./common/tools/ +COPY src/context/__init__.py context/__init__.py +COPY src/context/client/. context/client/ +COPY src/device/__init__.py device/__init__.py +COPY src/device/client/. device/client/ +COPY src/monitoring/__init__.py monitoring/__init__.py +COPY src/monitoring/client/. monitoring/client/ +COPY src/service/__init__.py service/__init__.py +COPY src/service/client/. service/client/ +COPY src/slice/__init__.py slice/__init__.py +COPY src/slice/client/. slice/client/ +COPY src/vnt_manager/__init__.py vnt_manager/__init__.py +COPY src/vnt_manager/client/. vnt_manager/client/ +COPY src/tests/*.py ./tests/ +COPY src/tests/acl_end2end/__init__.py ./tests/acl_end2end/__init__.py +COPY src/tests/acl_end2end/data/. ./tests/acl_end2end/data/ +COPY src/tests/acl_end2end/tests/. ./tests/acl_end2end/tests/ +COPY src/tests/acl_end2end/scripts/. ./ + +RUN apt-get --yes --quiet --quiet update && \ + apt-get --yes --quiet --quiet install tree && \ + rm -rf /var/lib/apt/lists/* + +RUN tree -la /var/teraflow diff --git a/src/tests/acl_end2end/README.md b/src/tests/acl_end2end/README.md new file mode 100644 index 0000000000000000000000000000000000000000..fe3718bea09ecba01022c1f53604ece04dd2acf8 --- /dev/null +++ b/src/tests/acl_end2end/README.md @@ -0,0 +1,123 @@ +# Control an Arista Firewall through TeraFlowSDN + +## Emulated DataPlane Deployment +- ContainerLab +- Scenario +- Descriptor + +## TeraFlowSDN Deployment +```bash +cd ~/tfs-ctrl +source ~/tfs-ctrl/src/tests/acl_end2end/deploy_specs.sh +./deploy/all.sh +``` + +# ContainerLab - Arista cEOS - Commands + +## Download and install ContainerLab +```bash +sudo bash -c "$(curl -sL https://get.containerlab.dev)" -- -v 0.59.0 +``` + +## Download Arista cEOS image and create Docker image +```bash +cd ~/tfs-ctrl/src/tests/acl_end2end/ +docker import arista/cEOS64-lab-4.32.2F.tar ceos:4.32.2F +``` + +## Deploy scenario +```bash +cd ~/tfs-ctrl/src/tests/acl_end2end/ +sudo containerlab deploy --topo acl_end2end.clab.yml +``` + +## Inspect scenario +```bash +cd ~/tfs-ctrl/src/tests/acl_end2end/ +sudo containerlab inspect --topo acl_end2end.clab.yml +``` + +## Destroy scenario +```bash +cd ~/tfs-ctrl/src/tests/acl_end2end/ +sudo containerlab destroy --topo acl_end2end.clab.yml +sudo rm -rf clab-acl_end2end/ .acl_end2end.clab.yml.bak +``` + +## Access cEOS Bash/CLI +```bash +docker exec -it clab-acl_end2end-firewall bash +docker exec -it clab-acl_end2end-firewall Cli +docker exec -it clab-acl_end2end-client1 bash +docker exec -it clab-acl_end2end-client2 bash +docker exec -it clab-acl_end2end-dc bash +``` + +## Configure ContainerLab clients +```bash +docker exec -it clab-acl_end2end-dc bash + ip address add 172.16.10.10/24 dev eth1 + ip route add 172.16.11.0/24 via 172.16.10.1 + ip route add 172.16.12.0/24 via 172.16.10.1 + ping 172.16.11.10 + ping 172.16.12.10 + +docker exec -it clab-acl_end2end-client1 bash + ip address add 172.16.11.10/24 dev eth1 + ip route add 172.16.10.0/24 via 172.16.11.1 + ip route add 172.16.12.0/24 via 172.16.11.1 + ping 172.16.10.10 + + +docker exec -it clab-acl_end2end-client2 bash + ip address add 172.16.12.10/24 dev eth1 + ip route add 172.16.10.0/24 via 172.16.12.1 + ip route add 172.16.11.0/24 via 172.16.12.1 + ping 172.16.10.10 +``` + +## Install gNMIc +```bash +sudo bash -c "$(curl -sL https://get-gnmic.kmrd.dev)" +``` + +## gNMI Capabilities request +```bash +gnmic --address clab-acl_end2end-firewall --port 6030 --username admin --password admin --insecure capabilities +``` + +## gNMI Get request +```bash +gnmic --address clab-acl_end2end-firewall --port 6030 --username admin --password admin --insecure --encoding json_ietf get --path / > firewall.json +gnmic --address clab-acl_end2end-firewall --port 6030 --username admin --password admin --insecure --encoding json_ietf get --path /interfaces/interface > firewall-ifaces.json +``` + +## gNMI Set request +```bash +gnmic --address clab-acl_end2end-firewall --port 6030 --username admin --password admin --insecure --encoding json_ietf set --update-path /system/config/hostname --update-value srl11 +gnmic --address clab-acl_end2end-firewall --port 6030 --username admin --password admin --insecure --encoding json_ietf get --path /system/config/hostname +``` + +## Subscribe request +```bash +gnmic --address clab-acl_end2end-firewall --port 6030 --username admin --password admin --insecure --encoding json_ietf subscribe --path /interfaces/interface[name=Management0]/state/ + +# In another terminal, you can generate traffic opening SSH connection +ssh admin@clab-acl_end2end-firewall +``` + +# Check configurations done: +```bash +gnmic --address clab-acl_end2end-firewall --port 6030 --username admin --password admin --insecure --encoding json_ietf get --path '/network-instances' > firewall-nis.json +gnmic --address clab-acl_end2end-firewall --port 6030 --username admin --password admin --insecure --encoding json_ietf get --path '/interfaces' > firewall-ifs.json +``` + +# ACL payload +`data/ietf-acl.json` contains an example ACL that blocks ICMP from client1 (172.16.11.10) to the DC (172.16.10.10) on the firewall ingress interface Ethernet11 while permitting other traffic. Post it to `/restconf/data/device=firewall/ietf-access-control-list:acls`. + +# Delete elements: +```bash +--address clab-acl_end2end-firewall --port 6030 --username admin --password admin --insecure --encoding json_ietf set --delete '/network-instances/network-instance[name=b19229e8]' +--address clab-acl_end2end-firewall --port 6030 --username admin --password admin --insecure --encoding json_ietf set --delete '/interfaces/interface[name=ethernet-1/1]/subinterfaces/subinterface[index=0]' +--address clab-acl_end2end-firewall --port 6030 --username admin --password admin --insecure --encoding json_ietf set --delete '/interfaces/interface[name=ethernet-1/2]/subinterfaces/subinterface[index=0]' +``` diff --git a/src/tests/acl_end2end/__init__.py b/src/tests/acl_end2end/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..3ccc21c7db78aac26daa1f8c5ff8e1ffd3f35460 --- /dev/null +++ b/src/tests/acl_end2end/__init__.py @@ -0,0 +1,14 @@ +# 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. + diff --git a/src/tests/acl_end2end/clab/acl_end2end.clab.yml b/src/tests/acl_end2end/clab/acl_end2end.clab.yml new file mode 100644 index 0000000000000000000000000000000000000000..737dd1f9db7ca7a96ff4fde6e95dae893392f1e6 --- /dev/null +++ b/src/tests/acl_end2end/clab/acl_end2end.clab.yml @@ -0,0 +1,74 @@ +# 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. + +# TFS - Arista devices + Linux clients + +name: acl_end2end + +mgmt: + network: mgmt-net + ipv4-subnet: 172.20.20.0/24 + +topology: + kinds: + arista_ceos: + kind: arista_ceos + #image: ceos:4.30.4M + #image: ceos:4.31.2F + #image: ceos:4.31.5M # tested, works + #image: ceos:4.32.0F + image: ceos:4.32.2F # tested, works + #image: ceos:4.32.2.1F + #image: ceos:4.33.1F # does not work, libyang.util.LibyangError: failed to parse data tree: No module named "openconfig-platform-healthz" in the context. + linux: + kind: linux + image: ghcr.io/hellt/network-multitool:latest + + nodes: + firewall: + kind: arista_ceos + mgmt-ipv4: 172.20.20.101 + startup-config: firewall-startup.cfg + + dc: + kind: linux + mgmt-ipv4: 172.20.20.200 + exec: + - ip link set address 00:c1:ab:00:0a:0a dev eth1 + - ip address add 172.16.10.10/24 dev eth1 + - ip route add 172.16.11.0/24 via 172.16.10.1 + - ip route add 172.16.12.0/24 via 172.16.10.1 + + client1: + kind: linux + mgmt-ipv4: 172.20.20.201 + exec: + - ip link set address 00:c1:ab:00:0b:0a dev eth1 + - ip address add 172.16.11.10/24 dev eth1 + - ip route add 172.16.10.0/24 via 172.16.11.1 + - ip route add 172.16.12.0/24 via 172.16.11.1 + + client2: + kind: linux + mgmt-ipv4: 172.20.20.202 + exec: + - ip link set address 00:c1:ab:00:0c:0a dev eth1 + - ip address add 172.16.12.10/24 dev eth1 + - ip route add 172.16.10.0/24 via 172.16.12.1 + - ip route add 172.16.11.0/24 via 172.16.12.1 + + links: + - endpoints: ["firewall:eth10", "dc:eth1" ] + - endpoints: ["firewall:eth11", "client1:eth1"] + - endpoints: ["firewall:eth12", "client2:eth1"] diff --git a/src/tests/acl_end2end/clab/firewall-startup.cfg b/src/tests/acl_end2end/clab/firewall-startup.cfg new file mode 100644 index 0000000000000000000000000000000000000000..274ecc71e2d4c463d17c6154481e713c341cfdef --- /dev/null +++ b/src/tests/acl_end2end/clab/firewall-startup.cfg @@ -0,0 +1,59 @@ +! device: firewall (cEOSLab, EOS-4.32.2F-38195967.4322F (engineering build)) +! +no aaa root +! +username admin privilege 15 role network-admin secret sha512 $6$OmfaAwJRg/r44r5U$9Fca1O1G6Bgsd4NKwSyvdRJcHHk71jHAR3apDWAgSTN/t/j1iroEhz5J36HjWjOF/jEVC/R8Wa60VmbX6.cr70 +! +management api http-commands + no shutdown +! +no service interface inactive port-id allocation disabled +! +transceiver qsfp default-mode 4x10G +! +service routing protocols model multi-agent +! +hostname firewall +! +spanning-tree mode mstp +! +system l1 + unsupported speed action error + unsupported error-correction action error +! +management api gnmi + transport grpc default +! +management api netconf + transport ssh default +! +interface Ethernet10 + no switchport + ip address 172.16.10.1/24 + no shutdown +! +interface Ethernet11 + no switchport + ip address 172.16.11.1/24 + no shutdown +! +interface Ethernet12 + no switchport + ip address 172.16.12.1/24 + no shutdown +! +interface Management0 + ip address 172.20.20.101/24 +! +ip routing +! +ip route 0.0.0.0/0 172.20.20.1 +! +router multicast + ipv4 + software-forwarding kernel + ! + ipv6 + software-forwarding kernel +! +end diff --git a/src/tests/acl_end2end/data/ietf-acl.json b/src/tests/acl_end2end/data/ietf-acl.json new file mode 100644 index 0000000000000000000000000000000000000000..39ea4ab85da701255a260df40bf8cd0d1471f05b --- /dev/null +++ b/src/tests/acl_end2end/data/ietf-acl.json @@ -0,0 +1,86 @@ +{ + "ietf-access-control-list:acls": { + "acl": [ + { + "name": "eth10-permit-other", + "type": "ipv4-acl-type", + "aces": { + "ace": [{ + "name": "eth10-permit-other", + "matches": { + "ingress-interface": "Ethernet10" + }, + "actions": {"forwarding": "accept"} + }] + } + }, + { + "name": "eth11-block-ping-permit-other", + "type": "ipv4-acl-type", + "aces": { + "ace": [{ + "name": "eth11-block-ping", + "matches": { + "ingress-interface": "Ethernet11", + "ipv4": { + "protocol": 1, + "source-ipv4-network": "172.16.11.10/32", + "destination-ipv4-network": "172.16.10.10/32" + }, + "icmp": {} + }, + "actions": {"forwarding": "reject"} + }, + { + "name": "eth11-permit-other", + "matches": { + "ingress-interface": "Ethernet11" + }, + "actions": {"forwarding": "accept"} + }] + } + }, + { + "name": "eth12-permit-other", + "type": "ipv4-acl-type", + "aces": { + "ace": [{ + "name": "eth12-permit-other", + "matches": { + "ingress-interface": "Ethernet12" + }, + "actions": {"forwarding": "accept"} + }] + } + } + ], + "attachment-points": { + "interface": [ + { + "interface-id": "Ethernet10", + "ingress": { + "acl-sets": { + "acl-set": [{"name": "eth10-permit-other"}] + } + } + }, + { + "interface-id": "Ethernet11", + "ingress": { + "acl-sets": { + "acl-set": [{"name": "eth11-block-ping-permit-other"}] + } + } + }, + { + "interface-id": "Ethernet12", + "ingress": { + "acl-sets": { + "acl-set": [{"name": "eth12-permit-other"}] + } + } + } + ] + } + } +} diff --git a/src/tests/acl_end2end/data/tfs-topology.json b/src/tests/acl_end2end/data/tfs-topology.json new file mode 100644 index 0000000000000000000000000000000000000000..6875b5075b6ff61db4c7897a377a6b2d10b99ddd --- /dev/null +++ b/src/tests/acl_end2end/data/tfs-topology.json @@ -0,0 +1,100 @@ +{ + "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": "client1"}}, "device_type": "emu-client", + "device_drivers": ["DEVICEDRIVER_UNDEFINED"], + "device_config": {"config_rules": [ + {"action": "CONFIGACTION_SET", "custom": {"resource_key": "_connect/address", "resource_value": "127.0.0.1"}}, + {"action": "CONFIGACTION_SET", "custom": {"resource_key": "_connect/port", "resource_value": "0"}}, + {"action": "CONFIGACTION_SET", "custom": {"resource_key": "_connect/settings", "resource_value": {"endpoints": [ + {"uuid": "eth1", "type": "copper"}, {"uuid": "int", "type": "copper"} + ]}}} + ]} + }, + { + "device_id": {"device_uuid": {"uuid": "client2"}}, "device_type": "emu-client", + "device_drivers": ["DEVICEDRIVER_UNDEFINED"], + "device_config": {"config_rules": [ + {"action": "CONFIGACTION_SET", "custom": {"resource_key": "_connect/address", "resource_value": "127.0.0.1"}}, + {"action": "CONFIGACTION_SET", "custom": {"resource_key": "_connect/port", "resource_value": "0"}}, + {"action": "CONFIGACTION_SET", "custom": {"resource_key": "_connect/settings", "resource_value": {"endpoints": [ + {"uuid": "eth1", "type": "copper"}, {"uuid": "int", "type": "copper"} + ]}}} + ]} + }, + { + "device_id": {"device_uuid": {"uuid": "dc"}}, "device_type": "emu-datacenter", + "device_drivers": ["DEVICEDRIVER_UNDEFINED"], + "device_config": {"config_rules": [ + {"action": "CONFIGACTION_SET", "custom": {"resource_key": "_connect/address", "resource_value": "127.0.0.1"}}, + {"action": "CONFIGACTION_SET", "custom": {"resource_key": "_connect/port", "resource_value": "0"}}, + {"action": "CONFIGACTION_SET", "custom": {"resource_key": "_connect/settings", "resource_value": {"endpoints": [ + {"uuid": "eth1", "type": "copper"}, {"uuid": "int", "type": "copper"} + ]}}} + ]} + }, + { + "device_id": {"device_uuid": {"uuid": "firewall"}}, "device_type": "packet-router", + "device_drivers": ["DEVICEDRIVER_GNMI_OPENCONFIG"], + "device_config": {"config_rules": [ + {"action": "CONFIGACTION_SET", "custom": {"resource_key": "_connect/address", "resource_value": "172.20.20.101"}}, + {"action": "CONFIGACTION_SET", "custom": {"resource_key": "_connect/port", "resource_value": "6030"}}, + {"action": "CONFIGACTION_SET", "custom": {"resource_key": "_connect/settings", "resource_value": { + "username": "admin", "password": "admin", "use_tls": false + }}} + ]} + } + ], + "links": [ + { + "link_id": {"link_uuid": {"uuid": "firewall/Ethernet10==dc/eth1"}}, + "link_endpoint_ids": [ + {"device_id": {"device_uuid": {"uuid": "firewall"}}, "endpoint_uuid": {"uuid": "Ethernet10"}}, + {"device_id": {"device_uuid": {"uuid": "dc"}}, "endpoint_uuid": {"uuid": "eth1"}} + ] + }, + { + "link_id": {"link_uuid": {"uuid": "dc/eth1==firewall/Ethernet10"}}, + "link_endpoint_ids": [ + {"device_id": {"device_uuid": {"uuid": "dc"}}, "endpoint_uuid": {"uuid": "eth1"}}, + {"device_id": {"device_uuid": {"uuid": "firewall"}}, "endpoint_uuid": {"uuid": "Ethernet10"}} + ] + }, + + { + "link_id": {"link_uuid": {"uuid": "firewall/Ethernet11==client1/eth1"}}, + "link_endpoint_ids": [ + {"device_id": {"device_uuid": {"uuid": "firewall"}}, "endpoint_uuid": {"uuid": "Ethernet11"}}, + {"device_id": {"device_uuid": {"uuid": "client1"}}, "endpoint_uuid": {"uuid": "eth1"}} + ] + }, + { + "link_id": {"link_uuid": {"uuid": "client1/eth1==firewall/Ethernet11"}}, + "link_endpoint_ids": [ + {"device_id": {"device_uuid": {"uuid": "client1"}}, "endpoint_uuid": {"uuid": "eth1"}}, + {"device_id": {"device_uuid": {"uuid": "firewall"}}, "endpoint_uuid": {"uuid": "Ethernet11"}} + ] + }, + + { + "link_id": {"link_uuid": {"uuid": "firewall/Ethernet12==client2/eth1"}}, + "link_endpoint_ids": [ + {"device_id": {"device_uuid": {"uuid": "firewall"}}, "endpoint_uuid": {"uuid": "Ethernet12"}}, + {"device_id": {"device_uuid": {"uuid": "client2"}}, "endpoint_uuid": {"uuid": "eth1"}} + ] + }, + { + "link_id": {"link_uuid": {"uuid": "client2/eth1==firewall/Ethernet12"}}, + "link_endpoint_ids": [ + {"device_id": {"device_uuid": {"uuid": "client2"}}, "endpoint_uuid": {"uuid": "eth1"}}, + {"device_id": {"device_uuid": {"uuid": "firewall"}}, "endpoint_uuid": {"uuid": "Ethernet12"}} + ] + } + ] +} diff --git a/src/tests/acl_end2end/deploy-scripts/clab-cli-client1.sh b/src/tests/acl_end2end/deploy-scripts/clab-cli-client1.sh new file mode 100755 index 0000000000000000000000000000000000000000..4c0a5740de15e774c4f4e76519df0e323f2ab719 --- /dev/null +++ b/src/tests/acl_end2end/deploy-scripts/clab-cli-client1.sh @@ -0,0 +1,16 @@ +#!/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. + +docker exec -it clab-acl_end2end-client1 bash diff --git a/src/tests/acl_end2end/deploy-scripts/clab-cli-client2.sh b/src/tests/acl_end2end/deploy-scripts/clab-cli-client2.sh new file mode 100644 index 0000000000000000000000000000000000000000..54ca743a4ebd7d2f86d3b616e138b64a8a13b600 --- /dev/null +++ b/src/tests/acl_end2end/deploy-scripts/clab-cli-client2.sh @@ -0,0 +1,16 @@ +#!/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. + +docker exec -it clab-acl_end2end-client2 bash diff --git a/src/tests/acl_end2end/deploy-scripts/clab-cli-dc.sh b/src/tests/acl_end2end/deploy-scripts/clab-cli-dc.sh new file mode 100755 index 0000000000000000000000000000000000000000..bde012b370fddb84a7ce9b50e26100b4b5959028 --- /dev/null +++ b/src/tests/acl_end2end/deploy-scripts/clab-cli-dc.sh @@ -0,0 +1,16 @@ +#!/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. + +docker exec -it clab-acl_end2end-dc bash diff --git a/src/tests/acl_end2end/deploy-scripts/clab-cli-firewall.sh b/src/tests/acl_end2end/deploy-scripts/clab-cli-firewall.sh new file mode 100755 index 0000000000000000000000000000000000000000..ad5ab0ce77e14298825d938744f3f3f20ab7a962 --- /dev/null +++ b/src/tests/acl_end2end/deploy-scripts/clab-cli-firewall.sh @@ -0,0 +1,16 @@ +#!/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. + +docker exec -it clab-acl_end2end-firewall Cli diff --git a/src/tests/acl_end2end/deploy-scripts/clab-deploy.sh b/src/tests/acl_end2end/deploy-scripts/clab-deploy.sh new file mode 100755 index 0000000000000000000000000000000000000000..7738f011dcad4d11ffd2d99273ba8e4e0381baf0 --- /dev/null +++ b/src/tests/acl_end2end/deploy-scripts/clab-deploy.sh @@ -0,0 +1,17 @@ +#!/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. + +cd ~/tfs-ctrl/src/tests/acl_end2end +sudo containerlab deploy --topo clab/acl_end2end.clab.yml diff --git a/src/tests/acl_end2end/deploy-scripts/clab-destroy.sh b/src/tests/acl_end2end/deploy-scripts/clab-destroy.sh new file mode 100755 index 0000000000000000000000000000000000000000..efcfcc09d92334510a82549f3256f22188d597c5 --- /dev/null +++ b/src/tests/acl_end2end/deploy-scripts/clab-destroy.sh @@ -0,0 +1,18 @@ +#!/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. + +cd ~/tfs-ctrl/src/tests/acl_end2end +sudo containerlab destroy --topo clab/acl_end2end.clab.yml +sudo rm -rf clab/clab-acl_end2end/ clab/.acl_end2end.clab.yml.bak diff --git a/src/tests/acl_end2end/deploy-scripts/clab-inspect.sh b/src/tests/acl_end2end/deploy-scripts/clab-inspect.sh new file mode 100755 index 0000000000000000000000000000000000000000..bec97444d7b38dc7d957149e82201800215c4674 --- /dev/null +++ b/src/tests/acl_end2end/deploy-scripts/clab-inspect.sh @@ -0,0 +1,17 @@ +#!/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. + +cd ~/tfs-ctrl/src/tests/acl_end2end +sudo containerlab inspect --topo acl_end2end.clab.yml diff --git a/src/tests/acl_end2end/deploy_specs.sh b/src/tests/acl_end2end/deploy_specs.sh new file mode 100755 index 0000000000000000000000000000000000000000..72cd25b58a02f442838bab866cc969680c073ebc --- /dev/null +++ b/src/tests/acl_end2end/deploy_specs.sh @@ -0,0 +1,208 @@ +#!/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. + + +# ----- TeraFlowSDN ------------------------------------------------------------ + +# Set the URL of the internal MicroK8s Docker registry where the images will be uploaded to. +export TFS_REGISTRY_IMAGES="http://localhost:32000/tfs/" + +# Set the list of components, separated by spaces, you want to build images for, and deploy. +#export TFS_COMPONENTS="context device pathcomp service slice nbi webui load_generator" +export TFS_COMPONENTS="context device pathcomp service nbi" + +# Uncomment to activate Monitoring (old) +#export TFS_COMPONENTS="${TFS_COMPONENTS} monitoring" + +# Uncomment to activate Monitoring Framework (new) +#export TFS_COMPONENTS="${TFS_COMPONENTS} kpi_manager kpi_value_writer kpi_value_api telemetry analytics automation" + +# Uncomment to activate QoS Profiles +#export TFS_COMPONENTS="${TFS_COMPONENTS} qos_profile" + +# Uncomment to activate BGP-LS Speaker +#export TFS_COMPONENTS="${TFS_COMPONENTS} bgpls_speaker" + +# Uncomment to activate Optical Controller +# To manage optical connections, "service" requires "opticalcontroller" to be deployed +# before "service", thus we "hack" the TFS_COMPONENTS environment variable prepending the +# "opticalcontroller" only if "service" is already in TFS_COMPONENTS, and re-export it. +#if [[ "$TFS_COMPONENTS" == *"service"* ]]; then +# BEFORE="${TFS_COMPONENTS% service*}" +# AFTER="${TFS_COMPONENTS#* service}" +# export TFS_COMPONENTS="${BEFORE} opticalcontroller service ${AFTER}" +#fi + +# Uncomment to activate ZTP +#export TFS_COMPONENTS="${TFS_COMPONENTS} ztp" + +# Uncomment to activate Policy Manager +#export TFS_COMPONENTS="${TFS_COMPONENTS} policy" + +# Uncomment to activate Optical CyberSecurity +#export TFS_COMPONENTS="${TFS_COMPONENTS} dbscanserving opticalattackmitigator opticalattackdetector opticalattackmanager" + +# Uncomment to activate L3 CyberSecurity +#export TFS_COMPONENTS="${TFS_COMPONENTS} l3_attackmitigator l3_centralizedattackdetector" + +# Uncomment to activate TE +#export TFS_COMPONENTS="${TFS_COMPONENTS} te" + +# Uncomment to activate Forecaster +#export TFS_COMPONENTS="${TFS_COMPONENTS} forecaster" + +# Uncomment to activate E2E Orchestrator +#export TFS_COMPONENTS="${TFS_COMPONENTS} e2e_orchestrator" + +# Uncomment to activate DLT and Interdomain +#export TFS_COMPONENTS="${TFS_COMPONENTS} interdomain dlt" +#if [[ "$TFS_COMPONENTS" == *"dlt"* ]]; then +# export KEY_DIRECTORY_PATH="src/dlt/gateway/keys/priv_sk" +# export CERT_DIRECTORY_PATH="src/dlt/gateway/keys/cert.pem" +# export TLS_CERT_PATH="src/dlt/gateway/keys/ca.crt" +#fi + +# Uncomment to activate QKD App +# To manage QKD Apps, "service" requires "qkd_app" to be deployed +# before "service", thus we "hack" the TFS_COMPONENTS environment variable prepending the +# "qkd_app" only if "service" is already in TFS_COMPONENTS, and re-export it. +#if [[ "$TFS_COMPONENTS" == *"service"* ]]; then +# BEFORE="${TFS_COMPONENTS% service*}" +# AFTER="${TFS_COMPONENTS#* service}" +# export TFS_COMPONENTS="${BEFORE} qkd_app service ${AFTER}" +#fi + + +# Set the tag you want to use for your images. +export TFS_IMAGE_TAG="dev" + +# Set the name of the Kubernetes namespace to deploy TFS to. +export TFS_K8S_NAMESPACE="tfs" + +# Set additional manifest files to be applied after the deployment +export TFS_EXTRA_MANIFESTS="manifests/nginx_ingress_http.yaml" + +# Uncomment to monitor performance of components +#export TFS_EXTRA_MANIFESTS="${TFS_EXTRA_MANIFESTS} manifests/servicemonitors.yaml" + +# Uncomment when deploying Optical CyberSecurity +#export TFS_EXTRA_MANIFESTS="${TFS_EXTRA_MANIFESTS} manifests/cachingservice.yaml" + +# Set the new Grafana admin password +export TFS_GRAFANA_PASSWORD="admin123+" + +# Disable skip-build flag to rebuild the Docker images. +export TFS_SKIP_BUILD="" + + +# ----- CockroachDB ------------------------------------------------------------ + +# Set the namespace where CockroackDB will be deployed. +export CRDB_NAMESPACE="crdb" + +# Set the external port CockroackDB Postgre SQL interface will be exposed to. +export CRDB_EXT_PORT_SQL="26257" + +# Set the external port CockroackDB HTTP Mgmt GUI interface will be exposed to. +export CRDB_EXT_PORT_HTTP="8081" + +# Set the database username to be used by Context. +export CRDB_USERNAME="tfs" + +# Set the database user's password to be used by Context. +export CRDB_PASSWORD="tfs123" + +# Set CockroachDB installation mode to 'single'. This option is convenient for development and testing. +# See ./deploy/all.sh or ./deploy/crdb.sh for additional details +export CRDB_DEPLOY_MODE="single" + +# Disable flag for dropping database, if it exists. +export CRDB_DROP_DATABASE_IF_EXISTS="YES" + +# Disable flag for re-deploying CockroachDB from scratch. +export CRDB_REDEPLOY="" + + +# ----- NATS ------------------------------------------------------------------- + +# Set the namespace where NATS will be deployed. +export NATS_NAMESPACE="nats" + +# Set the external port NATS Client interface will be exposed to. +export NATS_EXT_PORT_CLIENT="4222" + +# Set the external port NATS HTTP Mgmt GUI interface will be exposed to. +export NATS_EXT_PORT_HTTP="8222" + +# Set NATS installation mode to 'single'. This option is convenient for development and testing. +# See ./deploy/all.sh or ./deploy/nats.sh for additional details +export NATS_DEPLOY_MODE="single" + +# Disable flag for re-deploying NATS from scratch. +export NATS_REDEPLOY="" + + +# ----- QuestDB ---------------------------------------------------------------- + +# Set the namespace where QuestDB will be deployed. +export QDB_NAMESPACE="qdb" + +# Set the external port QuestDB Postgre SQL interface will be exposed to. +export QDB_EXT_PORT_SQL="8812" + +# Set the external port QuestDB Influx Line Protocol interface will be exposed to. +export QDB_EXT_PORT_ILP="9009" + +# Set the external port QuestDB HTTP Mgmt GUI interface will be exposed to. +export QDB_EXT_PORT_HTTP="9000" + +# Set the database username to be used for QuestDB. +export QDB_USERNAME="admin" + +# Set the database user's password to be used for QuestDB. +export QDB_PASSWORD="quest" + +# Set the table name to be used by Monitoring for KPIs. +export QDB_TABLE_MONITORING_KPIS="tfs_monitoring_kpis" + +# Set the table name to be used by Slice for plotting groups. +export QDB_TABLE_SLICE_GROUPS="tfs_slice_groups" + +# Disable flag for dropping tables if they exist. +export QDB_DROP_TABLES_IF_EXIST="YES" + +# Disable flag for re-deploying QuestDB from scratch. +export QDB_REDEPLOY="" + + +# ----- K8s Observability ------------------------------------------------------ + +# Set the external port Prometheus Mgmt HTTP GUI interface will be exposed to. +export PROM_EXT_PORT_HTTP="9090" + +# Set the external port Grafana HTTP Dashboards will be exposed to. +export GRAF_EXT_PORT_HTTP="3000" + + +# ----- Apache Kafka ----------------------------------------------------------- + +# Set the namespace where Apache Kafka will be deployed. +export KFK_NAMESPACE="kafka" + +# Set the port Apache Kafka server will be exposed to. +export KFK_SERVER_PORT="9092" + +# Set the flag to YES for redeploying of Apache Kafka +export KFK_REDEPLOY="" diff --git a/src/tests/acl_end2end/redeploy-tfs.sh b/src/tests/acl_end2end/redeploy-tfs.sh new file mode 100755 index 0000000000000000000000000000000000000000..614cbf2a79d88020bca900c2a796317265ae9041 --- /dev/null +++ b/src/tests/acl_end2end/redeploy-tfs.sh @@ -0,0 +1,17 @@ +#!/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. + +source ~/tfs-ctrl/src/tests/acl_end2end/deploy_specs.sh +./deploy/all.sh diff --git a/src/tests/acl_end2end/requirements.in b/src/tests/acl_end2end/requirements.in new file mode 100644 index 0000000000000000000000000000000000000000..5c92783a232a5bbe18b4dd6d0e6735e3ce8414c2 --- /dev/null +++ b/src/tests/acl_end2end/requirements.in @@ -0,0 +1,15 @@ +# 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. + +requests==2.27.* diff --git a/src/tests/acl_end2end/scripts/run-acl-create.sh b/src/tests/acl_end2end/scripts/run-acl-create.sh new file mode 100755 index 0000000000000000000000000000000000000000..97b8da8dbd52bc576df3c91c012175001ed10f62 --- /dev/null +++ b/src/tests/acl_end2end/scripts/run-acl-create.sh @@ -0,0 +1,20 @@ +#!/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. + +source /var/teraflow/tfs_runtime_env_vars.sh +export PYTHONPATH=/var/teraflow +pytest --verbose --log-level=INFO \ + --junitxml=/opt/results/report_acl_create.xml \ + /var/teraflow/tests/acl_end2end/tests/test_acl_create.py diff --git a/src/tests/acl_end2end/scripts/run-acl-delete.sh b/src/tests/acl_end2end/scripts/run-acl-delete.sh new file mode 100755 index 0000000000000000000000000000000000000000..c3450174a7a5fabed6d2bba485e9a4bb251cc01a --- /dev/null +++ b/src/tests/acl_end2end/scripts/run-acl-delete.sh @@ -0,0 +1,20 @@ +#!/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. + +source /var/teraflow/tfs_runtime_env_vars.sh +export PYTHONPATH=/var/teraflow +pytest --verbose --log-level=INFO \ + --junitxml=/opt/results/report_acl_delete.xml \ + /var/teraflow/tests/acl_end2end/tests/test_acl_delete.py diff --git a/src/tests/acl_end2end/scripts/run-cleanup.sh b/src/tests/acl_end2end/scripts/run-cleanup.sh new file mode 100755 index 0000000000000000000000000000000000000000..65329f32f958f68aed33f156ab557e4af6028b3e --- /dev/null +++ b/src/tests/acl_end2end/scripts/run-cleanup.sh @@ -0,0 +1,20 @@ +#!/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. + +source /var/teraflow/tfs_runtime_env_vars.sh +export PYTHONPATH=/var/teraflow +pytest --verbose --log-level=INFO \ + --junitxml=/opt/results/report_cleanup.xml \ + /var/teraflow/tests/acl_end2end/tests/test_cleanup.py diff --git a/src/tests/acl_end2end/scripts/run-onboarding.sh b/src/tests/acl_end2end/scripts/run-onboarding.sh new file mode 100755 index 0000000000000000000000000000000000000000..c01580ac4b6cd318e4beafb4c6451ec0b9fd3c59 --- /dev/null +++ b/src/tests/acl_end2end/scripts/run-onboarding.sh @@ -0,0 +1,20 @@ +#!/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. + +source /var/teraflow/tfs_runtime_env_vars.sh +export PYTHONPATH=/var/teraflow +pytest --verbose --log-level=INFO \ + --junitxml=/opt/results/report_onboarding.xml \ + /var/teraflow/tests/acl_end2end/tests/test_onboarding.py diff --git a/src/tests/acl_end2end/tests/Fixtures.py b/src/tests/acl_end2end/tests/Fixtures.py new file mode 100644 index 0000000000000000000000000000000000000000..5997e58c8100d8b89f2d9287fc57b9b3d1434ac4 --- /dev/null +++ b/src/tests/acl_end2end/tests/Fixtures.py @@ -0,0 +1,43 @@ +# 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. + +import pytest +from context.client.ContextClient import ContextClient +from device.client.DeviceClient import DeviceClient +from monitoring.client.MonitoringClient import MonitoringClient +from service.client.ServiceClient import ServiceClient + +@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 monitoring_client(): + _client = MonitoringClient() + yield _client + _client.close() + +@pytest.fixture(scope='session') +def service_client(): + _client = ServiceClient() + yield _client + _client.close() diff --git a/src/tests/acl_end2end/tests/Tools.py b/src/tests/acl_end2end/tests/Tools.py new file mode 100644 index 0000000000000000000000000000000000000000..bbee845cd57f8dcb57e19f1f8ecc71940e99df30 --- /dev/null +++ b/src/tests/acl_end2end/tests/Tools.py @@ -0,0 +1,109 @@ +# 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. + +import enum, logging, requests +from typing import Any, Dict, List, Optional, Set, Union +from common.Constants import ServiceNameEnum +from common.Settings import get_service_host, get_service_port_http + +NBI_ADDRESS = get_service_host(ServiceNameEnum.NBI) +NBI_PORT = get_service_port_http(ServiceNameEnum.NBI) +NBI_USERNAME = 'admin' +NBI_PASSWORD = 'admin' +NBI_BASE_URL = '' + +class RestRequestMethod(enum.Enum): + GET = 'get' + POST = 'post' + PUT = 'put' + PATCH = 'patch' + DELETE = 'delete' + +EXPECTED_STATUS_CODES : Set[int] = { + requests.codes['OK' ], + requests.codes['CREATED' ], + requests.codes['ACCEPTED' ], + requests.codes['NO_CONTENT'], +} + +def do_rest_request( + method : RestRequestMethod, url : str, body : Optional[Any] = None, timeout : int = 10, + allow_redirects : bool = True, expected_status_codes : Set[int] = EXPECTED_STATUS_CODES, + logger : Optional[logging.Logger] = None +) -> Optional[Union[Dict, List]]: + request_url = 'http://{:s}:{:s}@{:s}:{:d}{:s}{:s}'.format( + NBI_USERNAME, NBI_PASSWORD, NBI_ADDRESS, NBI_PORT, str(NBI_BASE_URL), url + ) + + if logger is not None: + msg = 'Request: {:s} {:s}'.format(str(method.value).upper(), str(request_url)) + if body is not None: msg += ' body={:s}'.format(str(body)) + logger.warning(msg) + reply = requests.request(method.value, request_url, timeout=timeout, json=body, allow_redirects=allow_redirects) + if logger is not None: + logger.warning('Reply: {:s}'.format(str(reply.text))) + assert reply.status_code in expected_status_codes, 'Reply failed with status code {:d}'.format(reply.status_code) + + if reply.content and len(reply.content) > 0: return reply.json() + return None + +def do_rest_get_request( + url : str, body : Optional[Any] = None, timeout : int = 10, + allow_redirects : bool = True, expected_status_codes : Set[int] = EXPECTED_STATUS_CODES, + logger : Optional[logging.Logger] = None +) -> Optional[Union[Dict, List]]: + return do_rest_request( + RestRequestMethod.GET, url, body=body, timeout=timeout, allow_redirects=allow_redirects, + expected_status_codes=expected_status_codes, logger=logger + ) + +def do_rest_post_request( + url : str, body : Optional[Any] = None, timeout : int = 10, + allow_redirects : bool = True, expected_status_codes : Set[int] = EXPECTED_STATUS_CODES, + logger : Optional[logging.Logger] = None +) -> Optional[Union[Dict, List]]: + return do_rest_request( + RestRequestMethod.POST, url, body=body, timeout=timeout, allow_redirects=allow_redirects, + expected_status_codes=expected_status_codes, logger=logger + ) + +def do_rest_put_request( + url : str, body : Optional[Any] = None, timeout : int = 10, + allow_redirects : bool = True, expected_status_codes : Set[int] = EXPECTED_STATUS_CODES, + logger : Optional[logging.Logger] = None +) -> Optional[Union[Dict, List]]: + return do_rest_request( + RestRequestMethod.PUT, url, body=body, timeout=timeout, allow_redirects=allow_redirects, + expected_status_codes=expected_status_codes, logger=logger + ) + +def do_rest_patch_request( + url : str, body : Optional[Any] = None, timeout : int = 10, + allow_redirects : bool = True, expected_status_codes : Set[int] = EXPECTED_STATUS_CODES, + logger : Optional[logging.Logger] = None +) -> Optional[Union[Dict, List]]: + return do_rest_request( + RestRequestMethod.PATCH, url, body=body, timeout=timeout, allow_redirects=allow_redirects, + expected_status_codes=expected_status_codes, logger=logger + ) + +def do_rest_delete_request( + url : str, body : Optional[Any] = None, timeout : int = 10, + allow_redirects : bool = True, expected_status_codes : Set[int] = EXPECTED_STATUS_CODES, + logger : Optional[logging.Logger] = None +) -> Optional[Union[Dict, List]]: + return do_rest_request( + RestRequestMethod.DELETE, url, body=body, timeout=timeout, allow_redirects=allow_redirects, + expected_status_codes=expected_status_codes, logger=logger + ) diff --git a/src/tests/acl_end2end/tests/__init__.py b/src/tests/acl_end2end/tests/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..3ccc21c7db78aac26daa1f8c5ff8e1ffd3f35460 --- /dev/null +++ b/src/tests/acl_end2end/tests/__init__.py @@ -0,0 +1,14 @@ +# 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. + diff --git a/src/tests/acl_end2end/tests/test_acl_create.py b/src/tests/acl_end2end/tests/test_acl_create.py new file mode 100644 index 0000000000000000000000000000000000000000..ff38434c22fffa8c5826d4f6c8161f3c6fafa191 --- /dev/null +++ b/src/tests/acl_end2end/tests/test_acl_create.py @@ -0,0 +1,54 @@ +# 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. + +import json, logging, os +from typing import Dict, List +from .Tools import do_rest_get_request, do_rest_post_request + +LOGGER = logging.getLogger(__name__) +LOGGER.setLevel(logging.DEBUG) + +ACL_URL = '/restconf/data/device=firewall/ietf-access-control-list:acls' +ACL_GET_URL_TEMPLATE = '/restconf/data/device=firewall/ietf-access-control-list:acl={acl_name}' +ACL_FILE = os.path.join( + os.path.dirname(os.path.abspath(__file__)), '..', 'data', 'ietf-acl.json' +) + + +def load_acl_payload() -> Dict: + with open(ACL_FILE, 'r', encoding='UTF-8') as f: + return json.load(f) + +def get_acl_names(payload: Dict) -> List[str]: + return [ + acl['name'] + for acl in payload['ietf-access-control-list:acls']['acl'] + ] + + +def test_ietf_acl_create() -> None: + acl_payload = load_acl_payload() + acl_names = get_acl_names(acl_payload) + + do_rest_post_request( + ACL_URL, body=acl_payload, logger=LOGGER, + expected_status_codes={201, 204} + ) + + for acl_name in acl_names: + response = do_rest_get_request( + ACL_GET_URL_TEMPLATE.format(acl_name=acl_name), logger=LOGGER, + expected_status_codes={200} + ) + assert response is not None diff --git a/src/tests/acl_end2end/tests/test_acl_delete.py b/src/tests/acl_end2end/tests/test_acl_delete.py new file mode 100644 index 0000000000000000000000000000000000000000..c52079ec5b92320db546bde556c80da3a1936826 --- /dev/null +++ b/src/tests/acl_end2end/tests/test_acl_delete.py @@ -0,0 +1,52 @@ +# 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. + +import json, logging, os +from typing import Dict, List +from .Tools import do_rest_delete_request, do_rest_get_request + +LOGGER = logging.getLogger(__name__) +LOGGER.setLevel(logging.DEBUG) + +ACL_URL = '/restconf/data/device=firewall/ietf-access-control-list:acls' +ACL_GET_URL_TEMPLATE = '/restconf/data/device=firewall/ietf-access-control-list:acl={acl_name}' +ACL_FILE = os.path.join( + os.path.dirname(os.path.abspath(__file__)), '..', 'data', 'ietf-acl.json' +) + + +def load_acl_payload() -> Dict: + with open(ACL_FILE, 'r', encoding='UTF-8') as f: + return json.load(f) + +def get_acl_names(payload: Dict) -> List[str]: + return [ + acl['name'] + for acl in payload['ietf-access-control-list:acls']['acl'] + ] + + +def test_ietf_acl_delete() -> None: + acl_payload = load_acl_payload() + acl_names = get_acl_names(acl_payload) + + for acl_name in acl_names: + do_rest_delete_request( + ACL_GET_URL_TEMPLATE.format(acl_name=acl_name), logger=LOGGER, + expected_status_codes={200, 204} + ) + do_rest_get_request( + ACL_GET_URL_TEMPLATE.format(acl_name=acl_name), logger=LOGGER, + expected_status_codes={404} + ) diff --git a/src/tests/acl_end2end/tests/test_cleanup.py b/src/tests/acl_end2end/tests/test_cleanup.py new file mode 100644 index 0000000000000000000000000000000000000000..20afb5fe02d63f64de45fe87830e8996302c4395 --- /dev/null +++ b/src/tests/acl_end2end/tests/test_cleanup.py @@ -0,0 +1,44 @@ +# 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. + +import logging, os +from common.Constants import DEFAULT_CONTEXT_NAME +from common.proto.context_pb2 import ContextId +from common.tools.descriptor.Loader import DescriptorLoader, validate_empty_scenario +from common.tools.object_factory.Context import json_context_id +from context.client.ContextClient import ContextClient +from device.client.DeviceClient import DeviceClient +from .Fixtures import context_client, device_client # pylint: disable=unused-import + +LOGGER = logging.getLogger(__name__) +LOGGER.setLevel(logging.DEBUG) + +DESCRIPTOR_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'data', 'tfs-topology.json') +ADMIN_CONTEXT_ID = ContextId(**json_context_id(DEFAULT_CONTEXT_NAME)) + +def test_scenario_cleanup( + context_client : ContextClient, # pylint: disable=redefined-outer-name + device_client : DeviceClient, # pylint: disable=redefined-outer-name +) -> None: + # Verify the scenario has no services/slices + response = context_client.GetContext(ADMIN_CONTEXT_ID) + assert len(response.service_ids) == 0 + assert len(response.slice_ids) == 0 + + # Load descriptors and validate the base scenario + descriptor_loader = DescriptorLoader( + descriptors_file=DESCRIPTOR_FILE, context_client=context_client, device_client=device_client) + descriptor_loader.validate() + descriptor_loader.unload() + validate_empty_scenario(context_client) diff --git a/src/tests/acl_end2end/tests/test_onboarding.py b/src/tests/acl_end2end/tests/test_onboarding.py new file mode 100644 index 0000000000000000000000000000000000000000..763d7da171c99b781a6d25fc01e3c10c340bfb43 --- /dev/null +++ b/src/tests/acl_end2end/tests/test_onboarding.py @@ -0,0 +1,67 @@ +# 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. + +import logging, os, time +from common.Constants import DEFAULT_CONTEXT_NAME +from common.proto.context_pb2 import ContextId, DeviceOperationalStatusEnum, Empty +from common.tools.descriptor.Loader import DescriptorLoader, check_descriptor_load_results, validate_empty_scenario +from common.tools.object_factory.Context import json_context_id +from context.client.ContextClient import ContextClient +from device.client.DeviceClient import DeviceClient +from .Fixtures import context_client, device_client # pylint: disable=unused-import + +LOGGER = logging.getLogger(__name__) +LOGGER.setLevel(logging.DEBUG) + +DESCRIPTOR_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'data', 'tfs-topology.json') +ADMIN_CONTEXT_ID = ContextId(**json_context_id(DEFAULT_CONTEXT_NAME)) + +def test_scenario_onboarding( + context_client : ContextClient, # pylint: disable=redefined-outer-name + device_client : DeviceClient, # pylint: disable=redefined-outer-name +) -> None: + validate_empty_scenario(context_client) + + descriptor_loader = DescriptorLoader( + descriptors_file=DESCRIPTOR_FILE, context_client=context_client, device_client=device_client) + results = descriptor_loader.process() + check_descriptor_load_results(results, descriptor_loader) + descriptor_loader.validate() + + # Verify the scenario has no services/slices + response = context_client.GetContext(ADMIN_CONTEXT_ID) + assert len(response.service_ids) == 0 + assert len(response.slice_ids) == 0 + +def test_scenario_devices_enabled( + context_client : ContextClient, # pylint: disable=redefined-outer-name +) -> None: + """ + This test validates that the devices are enabled. + """ + DEVICE_OP_STATUS_ENABLED = DeviceOperationalStatusEnum.DEVICEOPERATIONALSTATUS_ENABLED + + num_devices = -1 + num_devices_enabled, num_retry = 0, 0 + while (num_devices != num_devices_enabled) and (num_retry < 10): + time.sleep(1.0) + response = context_client.ListDevices(Empty()) + num_devices = len(response.devices) + num_devices_enabled = 0 + for device in response.devices: + if device.device_operational_status != DEVICE_OP_STATUS_ENABLED: continue + num_devices_enabled += 1 + LOGGER.info('Num Devices enabled: {:d}/{:d}'.format(num_devices_enabled, num_devices)) + num_retry += 1 + assert num_devices_enabled == num_devices diff --git a/src/tests/eucnc24/deploy-scripts/clab-deploy.sh b/src/tests/eucnc24/deploy-scripts/clab-deploy.sh index 5e79b0253d0e0452e47eebff72fb5dde72b47dda..4ed529e4886136c3a377529732449d22469449ba 100755 --- a/src/tests/eucnc24/deploy-scripts/clab-deploy.sh +++ b/src/tests/eucnc24/deploy-scripts/clab-deploy.sh @@ -14,4 +14,4 @@ # limitations under the License. cd ~/tfs-ctrl/src/tests/eucnc24 -sudo containerlab deploy --topo eucnc24.clab.yml +sudo containerlab deploy --topo clab/eucnc24.clab.yml diff --git a/src/tests/eucnc24/deploy-scripts/clab-destroy.sh b/src/tests/eucnc24/deploy-scripts/clab-destroy.sh index 55dcf96dceaa762d3628a52f7d7c36911761507c..464b82df6595e2dd28428250d0eab1476cdced08 100755 --- a/src/tests/eucnc24/deploy-scripts/clab-destroy.sh +++ b/src/tests/eucnc24/deploy-scripts/clab-destroy.sh @@ -14,5 +14,5 @@ # limitations under the License. cd ~/tfs-ctrl/src/tests/eucnc24 -sudo containerlab destroy --topo eucnc24.clab.yml -sudo rm -rf clab-eucnc24/ .eucnc24.clab.yml.bak +sudo containerlab destroy --topo clab/eucnc24.clab.yml +sudo rm -rf clab/clab-eucnc24/ clab/.eucnc24.clab.yml.bak