```bash
source .oam/bin/activate
-docker compose -f smo/common/docker-compose.yaml up -d --wait
+docker compose -f infra/docker-compose.yaml up -d
+docker compose -f smo/common/docker-compose.yaml up -d
# optionally adjust the users.csv file to create new users
vim users.csv
docker compose -f smo/apps/docker-compose.yaml up -d
# the cpu load is low again, we can start a simulated network
+```
+
+#### Simulated network
+Before starting the simulated network, you need to locally build the docker images.
+This is because of copyright issues with the 3GPP YANG models.
+The build should be pretty straightforward. The repository containing the PyNTS code needs to be cloned and then a command needs to be ran for building the images. Run this from another terminal, in another folder, not in this repo.
+
+```bash
+git clone "https://gerrit.o-ran-sc.org/r/sim/o1-ofhmp-interfaces"
+cd o1-ofhmp-interfaces
+make build-all
+```
+After everything is built successfully, you can return to your solution folder here and start the network.
+```bash
docker compose -f network/docker-compose.yaml up -d
docker compose -f network/docker-compose.yaml restart pynts-o-du-o1
```
Usually the first ves:event gets lost. Please restart the O-DU docker container(s) to send a second ves:pnfRegistration.
```
-docker compose -f network/docker-compose.yaml restart ntsim-ng-o-du-1122
-python network/config.py
+docker compose -f network/docker-compose.yaml restart pynts-o-du-o1
```
-The python script configures the simulated O-DU and O-RU according to O-RAN hybrid architecture.
+The simulated O-DU and O-RUs are pre-configured according to O-RAN hybrid architecture.
O-RU - NETCONF Call HOME and NETCONF notifications
O-DU - ves:pnfRegistration and ves:fault, ves:heartbeat

-'True' indicated that the settings through SDN-R to the NETCONF server were
-successful.
-
SDN-R reads the fault events from DMaaP and processes them.
Finally the fault events are visible in ODLUX.
--- /dev/null
+################################################################################
+# Copyright 2025 highstreet technologies
+#
+# 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.
+#
+
+NETWORK_SUBNET_DHCP_IPv4=172.99.0.0/16
+NETWORK_GATEWAY_DHCP_IPv4=172.99.0.1
+NETWORK_DHCP_CONTAINER_IPv4=172.99.0.10
\ No newline at end of file
--- /dev/null
+{
+ "Dhcp4": {
+ "interfaces-config": {
+ "interfaces": [ "*" ]
+ },
+ # 1. Declare a client-class that triggers if Option[60] == "o-ran-ru2/PyNTS"
+ "client-classes": [
+ {
+ "name": "pynts-class",
+ "test": "substring(option[60].text,0,15) == 'o-ran-ru2/pynts'",
+ # "test": "substring(option[60].hex, 0, 30) == '6F2D72616E2D7275322F50794E5453'"
+ }
+ ],
+
+ "lease-database": {
+ "type": "memfile"
+ },
+ "subnet4": [
+ {
+ "subnet": "172.99.0.0/16",
+ "id": 1,
+ "pools": [
+ {
+ "pool": "172.99.3.100 - 172.99.3.200"
+ }
+ ],
+ "evaluate-additional-classes": [ "pynts-class" ],
+ "option-data": [
+ {
+ "name": "routers",
+ "data": "172.99.0.1"
+ },
+ {
+ "name": "domain-name-servers",
+ "data": "8.8.8.8, 8.8.4.4"
+ }
+ ]
+ }
+ ],
+ "option-data": [
+ {
+ "name": "vendor-encapsulated-options", #// This is Option 43
+ "code": 43, #// Redundant if 'name' is recognized, but often kept for clarity
+ "csv-format": false, #// Because we’re providing hex directly
+ "data": "81:04:AC:63:00:05:82:1F:63:6F:6E:74:72:6F:6C:6C:65:72:2E:64:63:6E:2E:73:6D:6F:2E:6F:2D:72:61:6E:2D:73:63:2E:6F:72:67:86:01:01:83:04:AC:63:00:06:84:22:76:65:73:2D:63:6F:6C:6C:65:63:74:6F:72:2E:64:63:6E:2E:73:6D:6F:2E:6F:2D:72:61:6E:2D:73:63:2E:6F:72:67:85:01:00",
+ "always-send": true
+ # TLVs Explanation:
+ # 81:04:AC:63:00:05
+ # - Type: 0x81 (Controller IP)
+ # - Length: 0x04 (4 bytes)
+ # - Value: AC:63:00:05 (172.99.0.5)
+
+ # 82:1F:63:6F:6E:74:72:6F:6C:6C:65:72:2E:64:63:6E:2E:73:6D:6F:2E:6F:2D:72:61:6E:2D:73:63:2E:6F:72:67
+ # - Type: 0x82 (Controller FQDN)
+ # - Length: 0x1F (31 bytes)
+ # - Value: controller.dcn.smo.o-ran-sc.org (ASCII encoded)
+
+ # 86:01:01
+ # - Type: 0x86 (Call Home Type)
+ # - Length: 0x01 (1 byte)
+ # - Value: 0x01 (Call Home over TLS)
+
+ # 83:04:AC:63:00:06
+ # - Type: 0x83 (Event Collector IP)
+ # - Length: 0x04 (4 bytes)
+ # - Value: AC:63:00:06 (172.99.0.6)
+
+ # 84:22:76:65:73:2D:63:6F:6C:6C:65:63:74:6F:72:2E:64:63:6E:2E:73:6D:6F:2E:6F:2D:72:61:6E:2D:73:63:2E:6F:72:67
+ # - Type: 0x84 (Event Collector FQDN)
+ # - Length: 0x22 (34 bytes)
+ # - Value: ves-collector.dcn.smo.o-ran-sc.org (ASCII encoded)
+
+ # 85:01:00
+ # - Type: 0x85 (Event-Collector Notification Format)
+ # - Length: 0x01 (1 byte)
+ # - Value: 0x00 (pnfRegistration in ONAP defined format)
+ }
+ ],
+ "loggers": [
+ {
+ "name": "kea-dhcp4",
+ "severity": "DEBUG",
+ "output_options": [
+ {
+ "output": "stdout"
+ }
+ ]
+ }
+ ]
+ }
+}
--- /dev/null
+################################################################################
+# Copyright 2025 highstreet technologies
+#
+# 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-tester/Dockerfile
+FROM python:3.9-slim
+
+# 1. Install libpcap for Scapy sniffing, and tcpdump for debugging
+RUN apt-get update && apt-get install -y --no-install-recommends \
+ libpcap-dev tcpdump iproute2 \
+ && rm -rf /var/lib/apt/lists/*
+
+# 2. Install Scapy
+RUN pip install scapy
+
+WORKDIR /app
+
+# 3. Copy your test script
+COPY test_dhcp.py .
+
+# 4. Copy a small entrypoint script
+COPY docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh
+RUN chmod +x /usr/local/bin/docker-entrypoint.sh
+
+# Make the entrypoint script run by default
+ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"]
+
+# We won't specify CMD here, so it can remain empty or be overridden
--- /dev/null
+#!/bin/sh
+#
+################################################################################
+# Copyright 2025 highstreet technologies
+#
+# 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.
+#
+# This script runs test_dhcp.py once, prints the result, then keeps
+# the container alive (by tailing /dev/null). That way you can
+# debug inside the container if needed.
+
+echo "[+] Starting DHCP test..."
+python3 /app/test_dhcp.py
+
+echo "[+] Test script finished. Container will remain running for debugging..."
+echo " You can run 'docker exec -it dhcp-tester bash' (or sh) to explore."
+
+# Keep the container alive indefinitely
+tail -f /dev/null
--- /dev/null
+#!/usr/bin/env python3
+
+################################################################################
+# Copyright 2025 highstreet technologies
+#
+# 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.
+#
+"""
+Scapy-based DHCP test script with Option 43 parsing.
+
+Sends DHCPDISCOVER, waits for DHCPOFFER, extracts vendor-specific Option 43 data,
+and prints all relevant information.
+"""
+
+from scapy.all import Ether, IP, UDP, BOOTP, DHCP, sendp, sniff, AsyncSniffer
+import time
+import sys
+
+# Mapping of Option 43 sub-option types
+OPTION_43_TYPES = {
+ 0x81: "Controller IP Address",
+ 0x82: "Controller FQDN",
+ 0x83: "Event Collector IP Address",
+ 0x84: "Event Collector FQDN",
+ 0x85: "PNF Registration Format",
+ 0x86: "NETCONF Call Home"
+}
+
+def parse_option_43(raw_bytes):
+ """Parses vendor-encapsulated Option 43 data."""
+ index = 0
+ parsed_data = {}
+
+ while index < len(raw_bytes):
+ if index + 2 > len(raw_bytes):
+ print("[!] Malformed Option 43 data (truncated)")
+ break
+
+ opt_type = raw_bytes[index] # First byte is the sub-option type
+ opt_len = raw_bytes[index + 1] # Second byte is the length
+
+ if index + 2 + opt_len > len(raw_bytes):
+ print(f"[!] Skipping invalid Option 43 sub-option {opt_type:#04x} (bad length)")
+ break
+
+ opt_value = raw_bytes[index + 2: index + 2 + opt_len] # Value field
+
+ if opt_type in [0x81, 0x83]: # IP Address (4 bytes)
+ if opt_len == 4:
+ parsed_value = ".".join(str(b) for b in opt_value)
+ else:
+ parsed_value = f"Invalid IP length ({opt_len})"
+
+ elif opt_type in [0x82, 0x84]: # FQDN (ASCII string)
+ try:
+ parsed_value = opt_value.decode("ascii")
+ except UnicodeDecodeError:
+ parsed_value = opt_value.hex() # Fallback to hex if not ASCII
+
+ elif opt_type in [0x85, 0x86]: # Single-byte flags
+ parsed_value = int(opt_value[0]) if opt_len == 1 else f"Invalid flag length ({opt_len})"
+
+ else:
+ parsed_value = opt_value.hex() # Unknown types are printed in hex
+
+ parsed_data[OPTION_43_TYPES.get(opt_type, f"Unknown Type {opt_type:#04x}")] = parsed_value
+
+ index += 2 + opt_len # Move to the next sub-option
+
+ return parsed_data
+
+def print_dhcp_options(dhcp_options):
+ """Print all DHCP options from a list of (option, value) tuples."""
+ for opt in dhcp_options:
+ if isinstance(opt, tuple):
+ if opt[0] == "vendor_specific": # Fix: Use the correct Scapy name for Option 43
+ print(" Option 43 (Vendor-Specific Information):")
+ parsed_43 = parse_option_43(opt[1]) # Convert raw bytes
+ for k, v in parsed_43.items():
+ print(f" {k}: {v}")
+ else:
+ print(f" Option {opt[0]}: {opt[1]}")
+
+def handle_packet(pkt):
+ """Callback to process incoming DHCP packets and detect DHCPOFFER."""
+ if DHCP in pkt:
+ dhcp_opts = pkt[DHCP].options
+ for opt in dhcp_opts:
+ if isinstance(opt, tuple) and opt[0] == "message-type":
+ if opt[1] in [2, "offer"]: # DHCPOFFER detected
+ server_ip = pkt[IP].src
+ offered_ip = pkt[BOOTP].yiaddr
+ print(f"[+] Received DHCPOFFER from {server_ip}")
+ print(f" Offered IP: {offered_ip}")
+ print(" Full DHCP options:")
+ print_dhcp_options(dhcp_opts)
+ return True
+ return False
+
+def test_dhcp():
+ # Start sniffing in *async* mode so we don't block.
+ sniff_thread = AsyncSniffer(
+ iface="eth0",
+ filter="udp and (port 67 or port 68)",
+ prn=handle_packet
+ )
+ sniff_thread.start()
+
+ time.sleep(1) # Give it a moment to get ready
+
+ # Now send DHCPDISCOVER
+ discover = (
+ Ether(src="02:50:02:99:00:01", dst="ff:ff:ff:ff:ff:ff")
+ / IP(src="0.0.0.0", dst="255.255.255.255")
+ / UDP(sport=68, dport=67)
+ / BOOTP(chaddr=b'\x02\x50\x02\x99\x00\x01', xid=0x99999999, flags=0x8000)
+ / DHCP(options=[
+ ("message-type", "discover"),
+ ("parameter-request-list", [43]), # Explicitly request Option 43
+ ("vendor_class_id", "o-ran-ru2/pynts"), # Option 60
+ "end"
+ ])
+ )
+ print("[*] Sending DHCPDISCOVER...")
+ sendp(discover, iface="eth0", verbose=False)
+
+ print("[*] Sniffing for 5 seconds...")
+ time.sleep(5)
+
+ sniff_thread.stop()
+ results = sniff_thread.results
+ print(f"[+] Captured {len(results)} packets in total")
+
+ for pkt in results:
+ if handle_packet(pkt):
+ print("[+] Test SUCCESS - Received a valid DHCPOFFER.")
+ sys.exit(0) # Exit with success code 0
+
+ print("[-] Test FAILED - No DHCPOFFER received.")
+ sys.exit(1) # Exit with failure code 1
+
+if __name__ == "__main__":
+ test_dhcp()
--- /dev/null
+################################################################################
+# Copyright 2025 highstreet technologies
+#
+# 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.
+#
+
+services:
+ dhcp-server:
+ container_name: dhcp-server
+ privileged: true
+ cap_add:
+ - NET_ADMIN
+ image: docker.cloudsmith.io/isc/docker/kea-dhcp4:2.7.5
+ volumes:
+ - ./dhcp-server/kea-dhcp4.conf:/etc/kea/kea-dhcp4.conf:ro
+ restart: unless-stopped
+ environment:
+ - KEA_LOGGER_DESTINATION=stdout
+ - KEA_LOGGER_SEVERITY=INFO
+ networks:
+ dhcp:
+ ipv4_address: ${NETWORK_DHCP_CONTAINER_IPv4}
+
+ dhcp-tester:
+ build:
+ context: ./dhcp-tester
+ container_name: dhcp-tester
+ privileged: true
+ cap_add:
+ - NET_RAW
+ - NET_ADMIN
+ depends_on:
+ - dhcp-server # ensures dhcp-server starts first
+ networks:
+ dhcp:
+ # no static IP, Docker will assign from ${NETWORK_SUBNET_DCN_IPv4}
+
+networks:
+ dhcp:
+ name: dhcp
+ driver: macvlan
+ driver_opts:
+ parent: ens34 # or whichever host interface you want
+ macvlan_mode: bridge
+ ipam:
+ config:
+ - subnet: ${NETWORK_SUBNET_DCN_IPv4}
+ gateway: ${NETWORK_GATEWAY_DHCP_IPv4}
+
NEXUS3_DOCKER_REPO=nexus3.o-ran-sc.org:10004/o-ran-sc/
LOCAL_DOCKER_REPO=
-PYNTS_VERSION=0.6.4
+PYNTS_VERSION=0.8.1
NETCONF_USERNAME=netconf
NETCONF_PASSWORD=netconf!
#
# no more versions needed! Compose spec supports all features w/o a version
-version: "3.8"
services:
odlux:
image: ${SDNC_WEB_IMAGE}
- "controller.dcn.${HTTP_DOMAIN}:${HOST_IP}"
healthcheck:
test: wget --no-verbose --tries=1 --spider http://localhost:${SDNC_REST_PORT}/ready || exit 1
- start_period: 60s
+ start_period: 10s
interval: 10s
timeout: 5s
- retries: 5
+ retries: 10
environment:
ENABLE_ODL_CLUSTER: false
ENABLE_OAUTH: ${SDNC_ENABLE_OAUTH}