Skip to content

Commit 9f26366

Browse files
committed
Draining stuff
1 parent b7cfafe commit 9f26366

File tree

7 files changed

+161
-43
lines changed

7 files changed

+161
-43
lines changed

intent_deployer/links.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,14 @@
88

99

1010
class Links:
11-
hosts: dict[str, str]
11+
switch_links: list[tuple[str, str]]
12+
host_links: list[tuple[str, str]]
1213

1314
def __init__(self, links, hosts):
1415
self.switch_links = [
1516
(
1617
Switches.name_from_id(l["src"]["device"]),
17-
Switches.name_from_id(l["src"]["device"]),
18+
Switches.name_from_id(l["dst"]["device"]),
1819
)
1920
for l in links["links"]
2021
]

intent_deployer/network_state.py

+70-8
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,11 @@
99
from intent_deployer.hosts import Hosts
1010
from intent_deployer.links import Links
1111
from intent_deployer.switches import Switches
12+
from intents.utils import OnosRoute
1213

1314

1415
@dataclass
15-
class Intent:
16+
class IntentPath:
1617
src: str
1718
dst: str
1819
switch_path: list[str]
@@ -21,23 +22,49 @@ class Intent:
2122
def path(self):
2223
return [self.src, *self.switch_path, self.dst]
2324

24-
def as_tuple(self) -> tuple[tuple[str, str], Intent]:
25+
def as_tuple(self) -> tuple[tuple[str, str], IntentPath]:
2526
return ((self.src, self.dst), self)
2627

2728
@classmethod
28-
def parse(cls, data: dict[str, Any]) -> Intent:
29-
src, *switch_path, dst = cast(list[str], data["paths"][0]["path"])
29+
def parse(cls, data: dict[str, Any]) -> IntentPath:
30+
src, *switch_path, dst = cast(list[str], data["path"])
3031
src = Hosts.name_from_mac(src.removesuffix("/None"))
3132
dst = Hosts.name_from_mac(dst.removesuffix("/None"))
3233
switch_path = [Switches.name_from_id(s) for s in switch_path]
3334

3435
return cls(src, dst, switch_path)
3536

37+
def as_onos_route(self, hosts: Hosts, switches: Switches) -> OnosRoute:
38+
src, dst = hosts.get_host_id(self.src), hosts.get_host_id(self.dst)
39+
switch_path = cast(list[str], [switches.get_switch_id(s) for s in self.switch_path])
40+
41+
return OnosRoute(key=f"{src}{dst}", route=[src, *switch_path, dst])
42+
43+
44+
@dataclass
45+
class Intent:
46+
"""A point-to-point intent or whatever"""
47+
48+
paths: list[IntentPath]
49+
50+
@classmethod
51+
def parse(cls, data: dict[str, Any]) -> Intent:
52+
paths = [IntentPath.parse(x) for x in data["paths"]]
53+
54+
return cls(paths)
55+
56+
def as_onos_intent(self, hosts: Hosts, switches: Switches) -> list[OnosRoute]:
57+
return [p.as_onos_route(hosts, switches) for p in self.paths]
58+
59+
60+
# TODO: keep track of bandwidth
61+
# TODO: move drain part of this to the drain intent?
62+
3663

3764
@dataclass
3865
class Topology:
3966
network: networkx.Graph
40-
intents: dict[tuple[str, str], Intent]
67+
intents: list[Intent]
4168

4269
@classmethod
4370
async def from_current_state(cls) -> Topology:
@@ -51,8 +78,43 @@ async def from_current_state(cls) -> Topology:
5178
json={"api_key": config.api_key},
5279
)
5380
resp.raise_for_status()
54-
intents = dict(
55-
Intent.parse(i).as_tuple() for i in resp.json()["routingList"]
56-
)
81+
intents = [Intent.parse(i) for i in resp.json()["routingList"]]
5782

5883
return cls(network, intents)
84+
85+
@staticmethod
86+
def _perform_reroute(
87+
network: networkx.Graph, path: list[str], drain_node: str
88+
) -> IntentPath:
89+
"""Reroute a `path` to avoid a node.
90+
91+
This currently splits the path on the `drain_node` we want to drain from, then
92+
finds the shortest path between the two sides avoiding `drain_node`
93+
94+
`network` should be the network *without* `drain_node`.
95+
"""
96+
node_idx = path.index(drain_node)
97+
lhs, rhs = path[:node_idx], path[node_idx + 1 :]
98+
99+
# TODO: we need to detect when it's impossible to reroute, or if a host
100+
# will be dropped, etc and warn/cancel the intent
101+
102+
reroute_path = networkx.shortest_path(network, lhs[-1], rhs[0])
103+
reroute_path = cast(list[str], reroute_path)
104+
new_path = lhs[:-1] + reroute_path + rhs[1:]
105+
return IntentPath(src=lhs[0], dst=rhs[-1], switch_path=new_path[1:-1])
106+
107+
def drain_node(self, node: str) -> Intent:
108+
"""Construct an intent that drains traffic from a node."""
109+
110+
without_node: networkx.Graph = self.network.copy()
111+
without_node.remove_node(node)
112+
113+
new_intent_paths = [
114+
Topology._perform_reroute(without_node, intent_path.path, node)
115+
for intent in self.intents
116+
for intent_path in intent.paths
117+
if node in intent_path.path
118+
]
119+
120+
return Intent(paths=new_intent_paths)

intent_deployer/notifiers.py

+14-9
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import logging
44
from dataclasses import dataclass
5-
from typing import Callable, Optional, Protocol, Type
5+
from typing import Callable, Optional, Protocol, Type, TYPE_CHECKING
66
import httpx
77

88
from yarl import URL
@@ -11,14 +11,19 @@
1111

1212
log: logging.Logger = logging.getLogger(__name__)
1313

14-
15-
class Notifier(Protocol):
16-
@classmethod
17-
def parse(cls, body: dict) -> Notifier:
18-
...
19-
20-
async def send(self, msg: str):
21-
...
14+
if TYPE_CHECKING:
15+
# BUG: in py3.9.7 data classes that inherit from typing.Protocol break so
16+
# only actually use Protocol for type checking, use a plain class otherwise
17+
class Notifier(Protocol):
18+
@classmethod
19+
def parse(cls, body: dict) -> Notifier:
20+
...
21+
22+
async def send(self, msg: str):
23+
...
24+
else:
25+
class Notifier:
26+
pass
2227

2328

2429
_notifiers: dict[str, Type[Notifier]] = {}

intents/drain.py

+61
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
from __future__ import annotations
2+
3+
import re
4+
from typing import Any
5+
6+
from pydantic.main import BaseModel
7+
8+
from intent_deployer import config
9+
from intent_deployer.compile import (
10+
Context,
11+
register_nile_handler,
12+
register_dialogflow_handler,
13+
register_intent_executor,
14+
)
15+
from intent_deployer.deploy import deploy_policy
16+
from intent_deployer.network_state import Topology
17+
from intent_deployer.hosts import Hosts
18+
from intent_deployer.parser import NileIntent
19+
from intent_deployer.switches import Switches
20+
from intent_deployer.types import IntentDeployException
21+
from intents.utils import PushIntentPolicy
22+
23+
24+
class DrainIntent(BaseModel):
25+
node: str
26+
27+
28+
@register_nile_handler("drainIntent")
29+
def handle_nile_intent(intent: NileIntent) -> DrainIntent:
30+
if len(intent.targets) != 1:
31+
raise IntentDeployException(
32+
"Exactly one target should be specified in the drain intent"
33+
)
34+
35+
node = intent.targets[0].name
36+
37+
return DrainIntent(node=node)
38+
39+
40+
@register_dialogflow_handler("drain")
41+
def handle_dialogflow_intent(params: dict[str, Any]) -> DrainIntent:
42+
return DrainIntent(node=params["node"])
43+
44+
45+
@register_intent_executor(DrainIntent, PushIntentPolicy)
46+
async def handle_drain_intent(ctx: Context, intent: DrainIntent):
47+
hosts = await Hosts.from_api()
48+
switches = await Switches.from_api()
49+
topology = await Topology.from_current_state()
50+
drain_intent = topology.drain_node(intent.node)
51+
52+
await ctx.confirm_if_needed(
53+
f"This will drain intents from {intent.node} by rerouting {len(drain_intent.paths)} flows",
54+
lambda: deploy_policy(
55+
config.ngcdi_url / "push_intent",
56+
PushIntentPolicy(
57+
api_key=config.api_key,
58+
routes=drain_intent.as_onos_intent(hosts, switches),
59+
),
60+
),
61+
)

intents/forwarding.py

+4-8
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
from __future__ import annotations
2-
from intent_deployer.dialogflow import DialogflowContext
32

43
import re
5-
from typing import Literal, TypedDict, Any
4+
from typing import Any
65

76
from pydantic.main import BaseModel
8-
import httpx
97

108
from intent_deployer import config
119
from intent_deployer.compile import (
@@ -19,7 +17,7 @@
1917
from intent_deployer.parser import NileIntent
2018
from intent_deployer.switches import Switches
2119
from intent_deployer.types import IntentDeployException
22-
from intents.utils import Route, PushIntentPolicy, get_possible_routes
20+
from intents.utils import OnosRoute, PushIntentPolicy, get_possible_routes
2321

2422

2523
class ForwardIntent(BaseModel):
@@ -30,6 +28,7 @@ class ForwardIntent(BaseModel):
3028

3129
ForwardIntent.update_forward_refs()
3230

31+
3332
@register_nile_handler("forwardIntent")
3433
def handle_nile_intent(intent: NileIntent) -> ForwardIntent:
3534
if len(intent.endpoints) != 1:
@@ -64,9 +63,6 @@ def handle_dialogflow_intent(params: dict[str, Any]) -> ForwardIntent:
6463
@register_intent_executor(ForwardIntent, PushIntentPolicy)
6564
async def handle_forward_intent(ctx: Context, intent: ForwardIntent):
6665

67-
# if isinstance(ctx, DialogflowContext) and (notifier := ctx.get_notifier()) is not None:
68-
# notifier.send("Heya")
69-
7066
hosts = await Hosts.from_api()
7167
switches = await Switches.from_api()
7268

@@ -97,7 +93,7 @@ async def handle_forward_intent(ctx: Context, intent: ForwardIntent):
9793
lambda: deploy_policy(
9894
config.ngcdi_url / "push_intent",
9995
PushIntentPolicy(
100-
api_key=config.api_key, routes=[Route(key=src_id + dst_id, route=route)]
96+
api_key=config.api_key, routes=[OnosRoute(key=src_id + dst_id, route=route)]
10197
),
10298
),
10399
)

intents/upgrade.py

+6-14
Original file line numberDiff line numberDiff line change
@@ -5,29 +5,21 @@
55
from intent_deployer.dialogflow import DialogflowContext
66

77
import logging
8-
import re
9-
from typing import Literal, TypedDict, Any
8+
from typing import Any
109

1110
from pydantic.main import BaseModel
12-
import httpx
1311

1412
# who designed this library?
1513
from osmclient.sol005.client import Client as OSMClient
16-
from osmclient.client import Client as make_osm_client
1714

1815
from intent_deployer import config
1916
from intent_deployer.compile import (
2017
Context,
21-
register_nile_handler,
2218
register_dialogflow_handler,
2319
register_intent_executor,
2420
)
2521
from intent_deployer.deploy import deploy_policy
26-
from intent_deployer.hosts import Hosts
27-
from intent_deployer.parser import NileIntent
28-
from intent_deployer.switches import Switches
29-
from intent_deployer.types import IntentDeployException, Policy
30-
from intents.utils import PushIntentPolicy, Route, get_possible_routes
22+
from intents.utils import PushIntentPolicy, OnosRoute
3123

3224
log: logging.Logger = logging.getLogger(__name__)
3325

@@ -61,7 +53,7 @@ def handle_dialogflow_downgrade_intent(params: dict[str, Any]) -> DowngradeInten
6153
up_reroute = PushIntentPolicy(
6254
api_key=config.api_key,
6355
routes=[
64-
Route(
56+
OnosRoute(
6557
key="00:00:00:00:00:06/None00:00:00:00:00:09/None",
6658
route=[
6759
"00:00:00:00:00:06/None",
@@ -77,7 +69,7 @@ def handle_dialogflow_downgrade_intent(params: dict[str, Any]) -> DowngradeInten
7769
down_reroute = PushIntentPolicy(
7870
api_key=config.api_key,
7971
routes=[
80-
Route(
72+
OnosRoute(
8173
key="00:00:00:00:00:06/None00:00:00:00:00:09/None",
8274
route=[
8375
"00:00:00:00:00:06/None",
@@ -235,7 +227,7 @@ async def perform_downgrade(ctx: Context):
235227

236228

237229
@register_intent_executor(UpgradeIntent)
238-
async def handle_upgrade_intent(ctx: Context, intent: UpgradeIntent):
230+
async def handle_upgrade_intent(ctx: Context, _intent: UpgradeIntent):
239231
src = "h6"
240232
dst = "h9"
241233

@@ -272,7 +264,7 @@ async def handle_upgrade_intent(ctx: Context, intent: UpgradeIntent):
272264

273265

274266
@register_intent_executor(DowngradeIntent)
275-
async def handle_downgrade_intent(ctx: Context, intent: DowngradeIntent):
267+
async def handle_downgrade_intent(ctx: Context, _intent: DowngradeIntent):
276268
src = "h6"
277269
dst = "h9"
278270

intents/utils.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,14 @@
99
from intent_deployer.types import IntentDeployException, Policy
1010

1111

12-
class Route(BaseModel):
12+
class OnosRoute(BaseModel):
1313
key: str
1414
route: list[str]
15+
weight: int = 1
1516

1617

1718
class PushIntentPolicy(Policy):
18-
routes: list[Route]
19+
routes: list[OnosRoute]
1920

2021

2122
class GetRoutes(TypedDict):

0 commit comments

Comments
 (0)