|
""" |
|
|
|
This is for test only. Try to refactor it for your project. |
|
|
|
|
|
""" |
|
|
|
|
|
# lib/ho_cli.py |
|
# --------------------------------------------- |
|
# Clean ONOS flow management + Mininet-WiFi CLI commands: |
|
# - ho bs1|bs2 : make-before-break handover (delete old HO flows, install new) |
|
# - resetflows [bs1] : delete ALL our flows (s1/bs1/bs2) and reinstall base flows |
|
# - hoshow : show current active slave and current serving target |
|
# |
|
# Notes: |
|
# - We tag ALL flows we install with a fixed cookie (MY_COOKIE). This lets us delete ONLY our flows |
|
# without touching ONOS system flows (LLDP/BDDP, discovery, etc.). |
|
# - We use delete-then-add for the HO flow, so every new flow replaces the previous one cleanly. |
|
# - Port mapping for your small topology (confirmed by `net`): |
|
# s1-eth1 <-> bs1-eth2 => P_S1_BS1 = 1 |
|
# s1-eth2 <-> bs2-eth2 => P_S1_BS2 = 2 |
|
# s1-eth3 <-> h1-eth0 => P_S1_HOST = 3 |
|
# bs1: access=1, uplink=2 ; bs2: access=1, uplink=2 |
|
# |
|
# Requirements: |
|
# - ONOS REST enabled on http://127.0.0.1:8181 (default credentials onos/rocks) |
|
|
|
import json |
|
import base64 |
|
import time |
|
import urllib.request |
|
import urllib.error |
|
|
|
from mn_wifi.cli import CLI as MNWIFI_CLI |
|
|
|
# ---------------- ONOS REST CONFIG ---------------- |
|
ONOS_USER = "onos" |
|
ONOS_PASS = "rocks" |
|
ONOS_BASE = "http://127.0.0.1:8181/onos/v1" |
|
|
|
# ---------------- TOPO CONSTANTS (SMALL TOPO) ---------------- |
|
S1 = "of:0000000000000001" |
|
BS1 = "of:0000000000000011" |
|
BS2 = "of:0000000000000012" |
|
|
|
P_S1_BS1 = "1" |
|
P_S1_BS2 = "2" |
|
P_S1_HOST = "3" |
|
|
|
UE_IP = "10.0.0.10/32" # UE on bond0 |
|
H1_IP = "10.0.0.1" # test host |
|
|
|
# Tag all our flows with this cookie so we can delete them cleanly |
|
MY_COOKIE = "0x5a5a5a5a" |
|
|
|
|
|
# ---------------- REST HELPERS ---------------- |
|
def _auth_header(): |
|
token = base64.b64encode(f"{ONOS_USER}:{ONOS_PASS}".encode()).decode() |
|
return {"Authorization": f"Basic {token}"} |
|
|
|
|
|
def onos_get(path: str, timeout_s: int = 5): |
|
req = urllib.request.Request(ONOS_BASE + path, method="GET") |
|
req.add_header("Accept", "application/json") |
|
for k, v in _auth_header().items(): |
|
req.add_header(k, v) |
|
with urllib.request.urlopen(req, timeout=timeout_s) as r: |
|
return json.loads(r.read().decode("utf-8", errors="ignore")) |
|
|
|
|
|
def onos_post(path: str, payload: dict, timeout_s: int = 5): |
|
data = json.dumps(payload).encode("utf-8") |
|
req = urllib.request.Request(ONOS_BASE + path, data=data, method="POST") |
|
req.add_header("Content-Type", "application/json") |
|
for k, v in _auth_header().items(): |
|
req.add_header(k, v) |
|
|
|
try: |
|
with urllib.request.urlopen(req, timeout=timeout_s) as r: |
|
body = r.read().decode("utf-8", errors="ignore") |
|
return r.status, body |
|
except urllib.error.HTTPError as e: |
|
body = e.read().decode("utf-8", errors="ignore") |
|
return e.code, body |
|
except Exception as e: |
|
return -1, str(e) |
|
|
|
|
|
def onos_delete(path: str, timeout_s: int = 5): |
|
req = urllib.request.Request(ONOS_BASE + path, method="DELETE") |
|
for k, v in _auth_header().items(): |
|
req.add_header(k, v) |
|
try: |
|
with urllib.request.urlopen(req, timeout=timeout_s) as r: |
|
body = r.read().decode("utf-8", errors="ignore") |
|
return r.status, body |
|
except urllib.error.HTTPError as e: |
|
body = e.read().decode("utf-8", errors="ignore") |
|
return e.code, body |
|
except Exception as e: |
|
return -1, str(e) |
|
|
|
|
|
def wait_onos_device(device_id: str, timeout_s: int = 20, poll_s: float = 0.5) -> bool: |
|
"""Wait until ONOS reports a device as available.""" |
|
deadline = time.time() + timeout_s |
|
while time.time() < deadline: |
|
try: |
|
data = onos_get(f"/devices/{device_id}") |
|
if bool(data.get("available", False)): |
|
return True |
|
except Exception: |
|
pass |
|
time.sleep(poll_s) |
|
return False |
|
|
|
|
|
def _cookie_to_hex(cookie_val): |
|
"""Normalize ONOS cookie (could be str or int) to lower-case hex string like '0x1234'.""" |
|
if cookie_val is None: |
|
return None |
|
if isinstance(cookie_val, str): |
|
s = cookie_val.strip().lower() |
|
# ensure it starts with 0x for comparison |
|
if s.startswith("0x"): |
|
return s |
|
try: |
|
return hex(int(s)) |
|
except Exception: |
|
return s |
|
try: |
|
return hex(int(cookie_val)).lower() |
|
except Exception: |
|
return None |
|
|
|
|
|
def delete_flows_by_cookie(device_id: str, cookie_hex: str) -> int: |
|
"""Delete ONLY flows on device_id that have cookie == cookie_hex.""" |
|
data = onos_get(f"/flows/{device_id}") |
|
flows = data.get("flows", []) |
|
deleted = 0 |
|
|
|
for f in flows: |
|
c_hex = _cookie_to_hex(f.get("cookie")) |
|
if c_hex and c_hex == cookie_hex.lower(): |
|
flow_id = f.get("id") |
|
if flow_id: |
|
st, _ = onos_delete(f"/flows/{device_id}/{flow_id}") |
|
if st in (200, 204): |
|
deleted += 1 |
|
return deleted |
|
|
|
|
|
def add_flow(device_id: str, priority: int, criteria: list, instructions: list, *, cookie_hex: str, permanent: bool = True): |
|
payload = { |
|
"deviceId": device_id, |
|
"cookie": cookie_hex, |
|
"priority": int(priority), |
|
"isPermanent": bool(permanent), |
|
"selector": {"criteria": criteria}, |
|
"treatment": {"instructions": instructions}, |
|
} |
|
return onos_post(f"/flows/{device_id}", payload) |
|
|
|
|
|
# ---------------- FLOW INSTALLERS (BASE + HO) ---------------- |
|
def install_base_flows_small_topo(serving_bs: str = "bs1", *, base_prio: int = 50000): |
|
""" |
|
Install deterministic base flows for the small topology: |
|
- BS1/BS2: bridge ARP+IPv4 between access(1) and uplink(2) |
|
- S1: uplink ARP+IPv4 from BS ports to host port |
|
- S1: downlink ARP host->serving, IPv4 host->UE->serving (IP-based anchor) |
|
""" |
|
serving_bs = serving_bs.lower().strip() |
|
if serving_bs not in ("bs1", "bs2"): |
|
serving_bs = "bs1" |
|
|
|
# Wait devices (prevents "first rule not initialized" race) |
|
for dev in (S1, BS1, BS2): |
|
if not wait_onos_device(dev, timeout_s=25): |
|
raise RuntimeError(f"ONOS device not available: {dev}") |
|
|
|
out_port = P_S1_BS1 if serving_bs == "bs1" else P_S1_BS2 |
|
|
|
# ---- BS bridge flows (higher prio, ETH_TYPE specific) ---- |
|
for dev in (BS1, BS2): |
|
# access(1)->uplink(2) |
|
add_flow(dev, base_prio, |
|
[{"type": "IN_PORT", "port": "1"}, {"type": "ETH_TYPE", "ethType": "0x0806"}], |
|
[{"type": "OUTPUT", "port": "2"}], |
|
cookie_hex=MY_COOKIE) |
|
add_flow(dev, base_prio, |
|
[{"type": "IN_PORT", "port": "1"}, {"type": "ETH_TYPE", "ethType": "0x0800"}], |
|
[{"type": "OUTPUT", "port": "2"}], |
|
cookie_hex=MY_COOKIE) |
|
|
|
# uplink(2)->access(1) |
|
add_flow(dev, base_prio, |
|
[{"type": "IN_PORT", "port": "2"}, {"type": "ETH_TYPE", "ethType": "0x0806"}], |
|
[{"type": "OUTPUT", "port": "1"}], |
|
cookie_hex=MY_COOKIE) |
|
add_flow(dev, base_prio, |
|
[{"type": "IN_PORT", "port": "2"}, {"type": "ETH_TYPE", "ethType": "0x0800"}], |
|
[{"type": "OUTPUT", "port": "1"}], |
|
cookie_hex=MY_COOKIE) |
|
|
|
# ---- S1 uplink (BS->HOST) ---- |
|
add_flow(S1, base_prio, |
|
[{"type": "IN_PORT", "port": P_S1_BS1}, {"type": "ETH_TYPE", "ethType": "0x0806"}], |
|
[{"type": "OUTPUT", "port": P_S1_HOST}], |
|
cookie_hex=MY_COOKIE) |
|
add_flow(S1, base_prio, |
|
[{"type": "IN_PORT", "port": P_S1_BS2}, {"type": "ETH_TYPE", "ethType": "0x0806"}], |
|
[{"type": "OUTPUT", "port": P_S1_HOST}], |
|
cookie_hex=MY_COOKIE) |
|
|
|
add_flow(S1, base_prio, |
|
[{"type": "IN_PORT", "port": P_S1_BS1}, {"type": "ETH_TYPE", "ethType": "0x0800"}], |
|
[{"type": "OUTPUT", "port": P_S1_HOST}], |
|
cookie_hex=MY_COOKIE) |
|
add_flow(S1, base_prio, |
|
[{"type": "IN_PORT", "port": P_S1_BS2}, {"type": "ETH_TYPE", "ethType": "0x0800"}], |
|
[{"type": "OUTPUT", "port": P_S1_HOST}], |
|
cookie_hex=MY_COOKIE) |
|
|
|
# ---- S1 downlink ARP host->serving ---- |
|
add_flow(S1, base_prio + 1000, |
|
[{"type": "IN_PORT", "port": P_S1_HOST}, {"type": "ETH_TYPE", "ethType": "0x0806"}], |
|
[{"type": "OUTPUT", "port": out_port}], |
|
cookie_hex=MY_COOKIE) |
|
|
|
# ---- S1 downlink IPv4 host->UE->serving (anchor rule) ---- |
|
add_flow(S1, base_prio + 2000, |
|
[{"type": "IN_PORT", "port": P_S1_HOST}, |
|
{"type": "ETH_TYPE", "ethType": "0x0800"}, |
|
{"type": "IPV4_DST", "ip": UE_IP}], |
|
[{"type": "OUTPUT", "port": out_port}], |
|
cookie_hex=MY_COOKIE) |
|
|
|
return {"base_serving": serving_bs, "cookie": MY_COOKIE} |
|
|
|
|
|
def install_ho_rules_s1(target_bs: str, *, ho_prio: int): |
|
"""Install only the HO-specific rules (downlink IPv4 + ARP host->serving) on S1.""" |
|
target_bs = target_bs.lower().strip() |
|
out_port = P_S1_BS1 if target_bs == "bs1" else P_S1_BS2 |
|
|
|
# Remove previous HO rules (but keep base flows) |
|
# We separate HO cookie from base cookie to delete just HO rules. |
|
HO_COOKIE = "0x5a5a5a5b" # different cookie for HO rules |
|
delete_flows_by_cookie(S1, HO_COOKIE) |
|
|
|
# HO ARP host->serving (slightly higher than base) |
|
st1, body1 = add_flow(S1, ho_prio + 1, |
|
[{"type": "IN_PORT", "port": P_S1_HOST}, {"type": "ETH_TYPE", "ethType": "0x0806"}], |
|
[{"type": "OUTPUT", "port": out_port}], |
|
cookie_hex=HO_COOKIE) |
|
|
|
# HO IPv4 downlink host->UE->serving |
|
st2, body2 = add_flow(S1, ho_prio, |
|
[{"type": "IN_PORT", "port": P_S1_HOST}, |
|
{"type": "ETH_TYPE", "ethType": "0x0800"}, |
|
{"type": "IPV4_DST", "ip": UE_IP}], |
|
[{"type": "OUTPUT", "port": out_port}], |
|
cookie_hex=HO_COOKIE) |
|
|
|
return (st1, body1, st2, body2) |
|
|
|
|
|
# ---------------- CLI ---------------- |
|
class HOCLI(MNWIFI_CLI): |
|
""" |
|
Commands: |
|
- ho bs1|bs2 |
|
- resetflows [bs1|bs2] |
|
- hoshow |
|
""" |
|
|
|
def __init__(self, *args, **kwargs): |
|
# Mininet CLI enters cmdloop inside super().__init__(), so initialize state first. |
|
self.ho_prio = 65000 |
|
self.current_serving = "bs1" |
|
super().__init__(*args, **kwargs) |
|
|
|
def do_hoshow(self, _line): |
|
sta = self.mn.get("sta1") |
|
active = sta.cmd("cat /sys/class/net/bond0/bonding/active_slave 2>/dev/null || true").strip() |
|
print(f"serving={self.current_serving}, ho_prio={self.ho_prio}, active_slave={active}") |
|
|
|
def do_resetflows(self, line): |
|
""" |
|
resetflows [bs1|bs2] |
|
- Deletes ONLY our flows (cookie-tagged) on s1/bs1/bs2, reinstalls base flows. |
|
- Optionally sets initial serving BS (default bs1). |
|
""" |
|
args = line.split() |
|
serving = args[0] if (len(args) == 1 and args[0] in ("bs1", "bs2")) else "bs1" |
|
|
|
# Delete all base flows (MY_COOKIE) from all devices |
|
try: |
|
d1 = delete_flows_by_cookie(S1, MY_COOKIE) |
|
d2 = delete_flows_by_cookie(BS1, MY_COOKIE) |
|
d3 = delete_flows_by_cookie(BS2, MY_COOKIE) |
|
|
|
# Also delete HO cookie rules if present |
|
delete_flows_by_cookie(S1, "0x5a5a5a5b") |
|
|
|
info = install_base_flows_small_topo(serving_bs=serving) |
|
|
|
# Prime ARP/neighbor after reset |
|
sta = self.mn.get("sta1") |
|
h1 = self.mn.get("h1") |
|
sta.cmd("ip neigh flush all || true") |
|
h1.cmd("ip neigh flush all || true") |
|
sta.cmd(f"ping -c 1 -I bond0 {H1_IP} >/dev/null 2>&1 || true") |
|
|
|
# Keep internal state aligned |
|
self.current_serving = serving |
|
|
|
print(f"resetflows: deleted s1={d1}, bs1={d2}, bs2={d3}, base_serving={info['base_serving']}") |
|
except Exception as e: |
|
print(f"resetflows error: {e}") |
|
|
|
def do_ho(self, line): |
|
""" |
|
ho bs1|bs2 |
|
Make-before-break HO: |
|
1) Update S1 downlink for UE (delete previous HO flows, install new) |
|
2) Switch bonding active slave on sta1 |
|
3) Flush neigh + prime ping to avoid TCP stalling |
|
""" |
|
args = line.split() |
|
if len(args) != 1 or args[0] not in ("bs1", "bs2"): |
|
print("Usage: ho bs1|bs2") |
|
return |
|
|
|
target = args[0] |
|
sta = self.mn.get("sta1") |
|
|
|
# Ensure monotonic priority |
|
self.ho_prio += 10 |
|
|
|
# 1) Core update (delete-then-add HO rules on S1) |
|
st1, body1, st2, body2 = install_ho_rules_s1(target, ho_prio=self.ho_prio) |
|
|
|
if st1 not in (200, 201) or st2 not in (200, 201): |
|
print(f"ONOS HO error: arp_status={st1}, ipv4_status={st2}") |
|
if body1: |
|
print(f"arp_body: {body1}") |
|
if body2: |
|
print(f"ipv4_body: {body2}") |
|
return |
|
|
|
# 2) UE switch |
|
target_if = "sta1-bs1" if target == "bs1" else "sta1-bs2" |
|
sta.cmd(f"sh -c 'echo {target_if} > /sys/class/net/bond0/bonding/active_slave'") |
|
|
|
# 3) Flush neighbor + prime |
|
sta.cmd("ip neigh flush all || true") |
|
sta.cmd(f"ping -c 1 -I bond0 {H1_IP} >/dev/null 2>&1 || true") |
|
|
|
self.current_serving = target |
|
|
|
active = sta.cmd("cat /sys/class/net/bond0/bonding/active_slave 2>/dev/null || true").strip() |
|
print(f"HO -> {target} (prio={self.ho_prio}, active_slave={active})") |