Commit 4a7fffd2 authored by Andrea Sgambelluri's avatar Andrea Sgambelluri
Browse files

Add ECOC26 demo test harness, PMPv1 test topology, and support scripts

Brings over the demo/test-support material this branch's own work has
relied on throughout, none of which touches production service code:

- src/tests/ecoc26_pluggables/: the test harness used to bootstrap the
  T1.1/T1.2/T2.1 pluggable topology, run the ZSM/alarm failure-notification
  tests, and drive the demo end to end (ensure_topology.py, ecoc26_deploy.sh,
  resolve_cluster_env.sh, the ZSM/NBI/pluggable test suites, and the NETCONF
  OC optical mock server used for local testing without real hardware).
- src/tests/PMPv1/: the original point-to-multipoint test topology
  (descriptors, node-agent NETCONF configs, deploy scripts, functional
  create/delete-service tests) that the P2MP work in this branch was
  developed and validated against.
- Root-level scripts: run_ecoc_test.sh / stop_ecoc20_test.sh (start/stop the
  demo test run), free_disk_space.sh (microk8s/docker cleanup utility),
  test_tapi.py (TAPI tooling import sanity check), sync_controller.sh (rsync
  helper for this demo's dev workflow), pytest.ini (registers the
  `integration` marker used by these tests), .gitattributes (Git LFS rules
  for the large binary fixtures under PMPv1/ofc24).
- scripts/monitor_kpis.{sh,py}: watches KPI values flowing through Kafka
  during a demo run.

Excluded: a handful of stray editor/rsync backup files (*.bak_*, *.old)
picked up by the original branch's history, and two clearly-accidental
artifacts (a stale `kubectl get ns -o json` dump and a scratch log file)
that were never meant to be committed.
parent f61b3a0c
Loading
Loading
Loading
Loading

.gitattributes

0 → 100644
+7 −0
Changes for .gitattributes: 7 added lines, 0 removed lines.
Original line number Diff line number Diff line
*.tar filter=lfs diff=lfs merge=lfs -text
*.tar.gz filter=lfs diff=lfs merge=lfs -text
src/tests/ofc24/or/*.tar filter=lfs diff=lfs merge=lfs -text
src/tests/PMPv1/xr_control/*.tar.gz filter=lfs diff=lfs merge=lfs -text
*.jar filter=lfs diff=lfs merge=lfs -text
*.csv filter=lfs diff=lfs merge=lfs -text
*.pcap filter=lfs diff=lfs merge=lfs -text

free_disk_space.sh

0 → 100755
+62 −0
Changes for free_disk_space.sh: 62 added lines, 0 removed lines.
Original line number Diff line number Diff line
#!/bin/bash
# Frees disk space on tfs-vm to relieve kubelet DiskPressure.
# Safe to re-run any time. Needs sudo (will prompt for password).

set -uo pipefail

echo "===== BEFORE ====="
df -h /

echo
echo "===== Docker: pruning unused images + build cache ====="
docker builder prune -af
docker system prune -af

echo
echo "===== microk8s: pruning unused containerd images ====="
sudo microk8s ctr images prune

echo
echo "===== Internal registry (localhost:32000): garbage-collecting old image blobs ====="
# This is the biggest single lever: every rebuild+push to a :dev tag leaves the
# previous manifest behind untagged, and its blobs never get swept without
# --delete-untagged. Left unchecked this grows without bound (found at 24G once).
registry_pod=$(kubectl get pods -n container-registry -l app=registry -o jsonpath='{.items[0].metadata.name}' 2>/dev/null)
if [ -z "$registry_pod" ]; then
    echo "Could not find the registry pod (namespace container-registry, label app=registry) — skipping."
else
    echo "Running garbage-collect --delete-untagged on $registry_pod ..."
    kubectl exec -n container-registry "$registry_pod" -- \
        registry garbage-collect --delete-untagged /etc/docker/registry/config.yml \
        > /tmp/registry_gc_output.txt 2>&1
    tail -3 /tmp/registry_gc_output.txt
    echo "(full output: /tmp/registry_gc_output.txt)"
fi

echo
echo "===== journald: clearing log history ====="
sudo journalctl --rotate
sudo journalctl --vacuum-time=1s

echo
echo "===== VS Code Server: removing old versions (keeping the one currently in use) ====="
active_dir=$(ps aux | grep -o '/home/tfs/\.vscode-server/cli/servers/Stable-[^/]*' | sort -u | head -1)
if [ -z "$active_dir" ]; then
    echo "Could not detect the active VS Code server version — skipping this step to be safe."
else
    echo "Keeping active version: $active_dir"
    for dir in ~/.vscode-server/cli/servers/Stable-*; do
        if [ "$dir" != "$active_dir" ]; then
            echo "Removing $dir"
            rm -rf "$dir"
        fi
    done
fi

echo
echo "===== AFTER ====="
df -h /

echo
echo "===== DiskPressure status ====="
kubectl describe node tfs-vm 2>/dev/null | grep -A1 "DiskPressure"

netconf_monitoring.py

0 → 100644
+164 −0
Changes for netconf_monitoring.py: 164 added lines, 0 removed lines.
Original line number Diff line number Diff line
"""import subprocess

container = "nat1"
xpath = "/components/component[name='port-1']/state/oper-status"

cmd = [
    "docker", "exec", container,
    "python", "/confd/bin/netconf-console",   # full path inside container
    "--host=10.30.7.66",
    "--port=2028",
    "--proto=tcp",
    "--get-config", "-x", xpath
]

result = subprocess.run(cmd, capture_output=True, text=True)

print("STDOUT:\n", result.stdout.strip())
print("STDERR:\n", result.stderr.strip())"""
# opticalcontroller/dscm_monitoring.py

import subprocess
import xmltodict
#import os
#os.environ['PATH'] += os.pathsep + '/usr/bin'

# Container that has confd + netconf-console installed
CONTAINER = "nat1"

HUB = {"host": "10.30.7.7", "port": 2023}
LEAFS = [
    {"host": "10.30.7.8", "port": 2023},
]

"""HUB = {"host": "10.30.7.66", "port": 2028}
LEAFS = [
    {"host": "10.30.7.66", "port": 2027},
]"""
FILTERS = {
    # Hub only has port-1 and channel-1
    "hub_status_port": '/components/component[name="port-1"]/state/oper-status',
    "hub_status_channel": '/components/component[name="channel-1"]/state/oper-status',
    
    # Leafs have ports 1, 3, 5 and corresponding channels
    "leaf_status_port-1": '/components/component[name="port-1"]/state/oper-status',
    "leaf_status_port-3": '/components/component[name="port-3"]/state/oper-status',
    "leaf_status_port-5": '/components/component[name="port-5"]/state/oper-status',
    "leaf_status_channel-1": '/components/component[name="channel-1"]/state/oper-status',
    "leaf_status_channel-3": '/components/component[name="channel-3"]/state/oper-status',
    "leaf_status_channel-5": '/components/component[name="channel-5"]/state/oper-status',

    # Parameter filters (with placeholders)
    "input_power": '/components/component[name="{ch}"]/optical-channel/state/input-power/instant',
    "output_power": '/components/component[name="{ch}"]/optical-channel/state/output-power/instant',
    "frequency": '/components/component[name="{ch}"]/optical-channel/state/frequency',
    "configured_freq": '/components/component[name="{ch}"]/optical-channel/config/frequency',
    "ber": '/components/component[name="{ch}"]/optical-channel/state/digital-subcarriers-group[1]/pre-fec-ber/instant',
    "osnr": '/components/component[name="{ch}"]/optical-channel/state/digital-subcarriers-group[1]/osnr/instant',
}


def run_netconf_console(host, port, xpath):
    """Run netconf-console inside the docker container and return XML reply."""
    cmd = [
        "docker", "exec", CONTAINER,
        "python2", "/confd/bin/netconf-console",
        "--host", host,
        "--port", str(port),
        "--proto", "tcp",
        "--get-config", "-x", xpath,
    ]
    result = subprocess.run(cmd, capture_output=True, text=True)
    if result.returncode != 0:
        raise RuntimeError(f"Command failed: {result.stderr}")
    return result.stdout.strip()


def get_value(host, port, xpath):
    """Extract a single leaf value from netconf-console XML reply."""
    try:
        xml_reply = run_netconf_console(host, port, xpath)
        data_dict = xmltodict.parse(xml_reply)

        # Try to walk down into <rpc-reply><data>...
        data = data_dict.get("rpc-reply", {}).get("data", {})
        if not data:
            return None

        # Return the deepest leaf value
        while isinstance(data, dict) and len(data) == 1:
            data = list(data.values())[0]
        if isinstance(data, str):
            return data
        return None
    except Exception as e:
        return f"Error: {e}"


def monitor_hub():
    """Monitor hub (10.30.7.7) - only port-1 and channel-1"""
    result = {"ports": {}, "channels": {}}
    
    # Check port status
    port_status = get_value(HUB["host"], HUB["port"], FILTERS["hub_status_port"])
    result["ports"]["port-1"] = {"status": port_status}
    
    # Check channel status
    ch_status = get_value(HUB["host"], HUB["port"], FILTERS["hub_status_channel"])
    ch_info = {"status": ch_status}

    if ch_status and ch_status.lower() in ["active", "up"]:
        ch_info["input_power"] = get_value(HUB["host"], HUB["port"], FILTERS["input_power"].format(ch="channel-1"))
        ch_info["output_power"] = get_value(HUB["host"], HUB["port"], FILTERS["output_power"].format(ch="channel-1"))
        ch_info["frequency"] = get_value(HUB["host"], HUB["port"], FILTERS["frequency"].format(ch="channel-1"))
        ch_info["ber"] = get_value(HUB["host"], HUB["port"], FILTERS["ber"].format(ch="channel-1"))
        ch_info["osnr"] = get_value(HUB["host"], HUB["port"], FILTERS["osnr"].format(ch="channel-1"))
        ch_info["link"] = "UP"
    else:
        ch_info["link"] = "DOWN"

    result["channels"]["channel-1"] = ch_info
    return result


def monitor_leaf(leaf):
    """Monitor leaf (10.30.7.8) - ports 1,3,5 and channels 1,3,5"""
    result = {"ports": {}, "channels": {}}
    
    # Monitor ports
    for port in ["port-1", "port-3", "port-5"]:
        port_status = get_value(leaf["host"], leaf["port"], FILTERS[f"leaf_status_{port}"])
        result["ports"][port] = {"status": port_status}
    
    # Monitor channels
    for ch in ["channel-1", "channel-3", "channel-5"]:
        ch_status = get_value(leaf["host"], leaf["port"], FILTERS[f"leaf_status_{ch}"])
        ch_info = {"status": ch_status}

        if ch_status and ch_status.lower() in ["active", "up"]:
            ch_info["input_power"] = get_value(leaf["host"], leaf["port"], FILTERS["input_power"].format(ch=ch))
            ch_info["output_power"] = get_value(leaf["host"], leaf["port"], FILTERS["output_power"].format(ch=ch))
            ch_info["frequency"] = get_value(leaf["host"], leaf["port"], FILTERS["frequency"].format(ch=ch))
            ch_info["ber"] = get_value(leaf["host"], leaf["port"], FILTERS["ber"].format(ch=ch))
            ch_info["osnr"] = get_value(leaf["host"], leaf["port"], FILTERS["osnr"].format(ch=ch))
            ch_info["link"] = "UP"
        else:
            ch_info["link"] = "DOWN"

        result["channels"][ch] = ch_info
    
    return result


def get_status_snapshot():
    """Return a snapshot of hub + leaf status as a dict."""
    snapshot = {"hub": None, "leafs": []}
    snapshot["hub"] = monitor_hub()
    for leaf in LEAFS:
        snapshot["leafs"].append({leaf["host"]: monitor_leaf(leaf)})
    return snapshot


if __name__ == "__main__":
    snapshot = get_status_snapshot()
    print(snapshot)
 No newline at end of file

pytest.ini

0 → 100644
+18 −0
Changes for pytest.ini: 18 added lines, 0 removed lines.
Original line number Diff line number Diff line
# 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.


[pytest]
markers =
    integration: tests that require real NETCONF devices or external infrastructure

redeploy.sh

0 → 100755
+69 −0
Changes for redeploy.sh: 69 added lines, 0 removed lines.
Original line number Diff line number Diff line
#!/bin/bash
# Rebuild, push, and redeploy one or more TFS components to the running
# microk8s cluster -- a thin wrapper around deploy/component.sh that can be
# called from any directory and prints a quick post-deploy pod/log check.
#
# Usage:
#   ./redeploy.sh <component> [<component> ...]
#
# Examples:
#   ./redeploy.sh opticalcontroller
#   ./redeploy.sh service webui
#   ./redeploy.sh policy
#
# Run with no arguments to see the component names this demo normally uses
# (see src/tests/ecoc26_pluggables/ecoc26_deploy.sh).

set -eu

REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$REPO_ROOT"

KNOWN_COMPONENTS="context device pathcomp opticalcontroller service nbi webui policy kpi_manager telemetry analytics automation pluggables"

if [[ $# -eq 0 ]]; then
    echo "Usage: $0 <component> [<component> ...]"
    echo
    echo "Components normally used by this demo:"
    echo "  ${KNOWN_COMPONENTS}"
    exit 1
fi

echo "Sourcing my_deploy.sh for registry/tag/namespace defaults..."
if ! source my_deploy.sh >/tmp/redeploy_my_deploy.log 2>&1; then
    echo "Failed to source my_deploy.sh -- see /tmp/redeploy_my_deploy.log"
    exit 1
fi

echo "Redeploying: $*"
./deploy/component.sh "$*"

echo
echo "── Post-deploy pod check ──────────────────────────────────────────────"
# Best-effort only from here on -- a transient microk8s API hiccup here must
# never be reported as a redeploy failure, since deploy/component.sh above
# already succeeded (set -e would have stopped this script otherwise).
set +e
sleep 3
for COMPONENT in "$@"; do
    # TFS deployment/pod names are "<component>service" with underscores
    # flattened to hyphens (e.g. kpi_manager -> kpi-managerservice) -- this
    # is a best-effort match, not authoritative. Excludes any pod with a
    # deletionTimestamp set: right after a redeploy the old pod is often
    # still Terminating alongside the new one, and its .status.phase still
    # reads "Running" even while kubectl's own STATUS column shows
    # "Terminating" (that column is derived from deletionTimestamp, not
    # phase) -- so filtering on phase alone would still risk picking it.
    PATTERN="$(echo "$COMPONENT" | tr '_' '-')"
    POD=$(kubectl -n tfs get pods -o custom-columns=NAME:.metadata.name,DELETED:.metadata.deletionTimestamp --no-headers 2>/dev/null \
          | awk -v pat="$PATTERN" 'tolower($1) ~ tolower(pat) && $2 == "<none>" {print $1}' | tail -1)
    if [[ -z "$POD" ]]; then
        echo "[$COMPONENT] could not find a matching pod -- check manually with: kubectl -n tfs get pods | grep -i $PATTERN"
        continue
    fi
    echo "[$COMPONENT] pod: $POD"
    kubectl -n tfs get pod "$POD"
    echo "  -- last 15 log lines --"
    kubectl -n tfs logs "$POD" --timestamps --tail=15 2>&1 | sed 's/^/  /'
    echo
done
Loading