-
Notifications
You must be signed in to change notification settings - Fork 36
Expand file tree
/
Copy pathwebapi.py
More file actions
230 lines (177 loc) · 7.39 KB
/
Copy pathwebapi.py
File metadata and controls
230 lines (177 loc) · 7.39 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
# Copyright (c) 2018-2026 Maen Artimy
#
# 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 module includes FastAPI application for FlowManager.
"""
import os
import asyncio
import logging
from typing import List, Optional
from fastapi import FastAPI, Request, WebSocket, WebSocketDisconnect, Query
from fastapi.responses import JSONResponse, FileResponse
from fastapi.staticfiles import StaticFiles
from fastapi.middleware.cors import CORSMiddleware
from models import FlowEntry, GroupEntry, MeterEntry, ConfigUpload
logger = logging.getLogger("flowmanager")
app = FastAPI(title="FlowManager API")
# Enable CORS
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_methods=["*"],
allow_headers=["*"],
)
# Global reference to ctrl_api
ctrl_api = None
class ConnectionManager:
"""Manages active WebSocket connections and broadcasts messages to all clients.
Replaces the old WebSocketRPCServer / rpc_clients pattern. The single shared
instance is stored on app.state so other modules can reach it via the FastAPI
app object, mirroring the old self.ctrl_api.app reference.
"""
def __init__(self):
self.active_connections: set[WebSocket] = set()
self._loop: asyncio.AbstractEventLoop | None = None
def set_loop(self, loop: asyncio.AbstractEventLoop):
"""Store the event loop so sync callers can schedule broadcasts onto it."""
self._loop = loop
def register(self, websocket: WebSocket):
self.active_connections.add(websocket)
def unregister(self, websocket: WebSocket):
self.active_connections.discard(websocket)
async def broadcast(self, message: str):
"""Send a message to every connected client, pruning stale connections."""
disconnected = set()
for ws in self.active_connections:
try:
await ws.send_text(message)
except Exception as e:
logger.error("Error broadcasting to websocket: %s", e)
disconnected.add(ws)
self.active_connections -= disconnected
def broadcast_sync(self, message: str):
"""Thread-safe broadcast for callers running outside the asyncio loop.
flowmanager.py runs in an OS-Ken/eventlet hub thread, so it cannot
await directly. This schedules the coroutine onto the stored loop.
"""
if self._loop is None or not self._loop.is_running():
logger.error("broadcast_sync called before event loop is available")
return
asyncio.run_coroutine_threadsafe(self.broadcast(message), self._loop)
# Single shared instance - registered on app.state in run_server()
manager = ConnectionManager()
def broadcast_sync(message: str):
"""Module-level convenience wrapper used by flowmanager.py."""
manager.broadcast_sync(message)
@app.get("/status")
async def get_flow_stats(status: str, dpid: str):
"""Get stats"""
if ctrl_api:
data = ctrl_api.get_stats(status, dpid)
return JSONResponse(content=data)
return JSONResponse(content={"error": "ctrl_api not initialized"}, status_code=500)
@app.get("/data")
async def get_switch_data(request: Request):
"""Get switch data"""
if not ctrl_api:
return JSONResponse(
content={"error": "ctrl_api not initialized"}, status_code=500
)
params = request.query_params
if params.get("list") == "switches":
lst = {t[0]: t[0] for t in ctrl_api.get_switches()}
else:
req_type = list(params.keys())[0]
dpid = int(params[req_type])
lst = ctrl_api.get_stats_request(req_type, dpid)
return JSONResponse(content=lst)
@app.get("/topology")
async def get_topology():
"""Get topology info"""
if ctrl_api:
return JSONResponse(content=ctrl_api.get_topology_data())
return JSONResponse(content={"error": "ctrl_api not initialized"}, status_code=500)
@app.get("/logs")
async def get_logs():
"""Get log messages"""
if ctrl_api:
return JSONResponse(content=ctrl_api.read_logs())
return JSONResponse(content={"error": "ctrl_api not initialized"}, status_code=500)
@app.post("/meterform")
async def post_meter_form(entry: MeterEntry):
"""Connect with meter form"""
return ctrl_api.process_meter_message(entry.dict(exclude_unset=True))
@app.post("/groupform")
async def post_group_form(entry: GroupEntry):
"""Connect with group form"""
return ctrl_api.process_group_message(entry.dict(exclude_unset=True))
@app.post("/flowform")
async def post_flow_form(entry: FlowEntry):
"""Connect with flow control form"""
return ctrl_api.process_flow_message(entry.dict(exclude_unset=True))
@app.post("/upload")
async def post_config_upload(config: ConfigUpload):
meters = config.meters
groups = config.groups
flows = config.flows
response_meters = ctrl_api.process_meter_upload(meters) if meters else ""
response_groups = ctrl_api.process_group_upload(groups) if groups else ""
response_flows = ctrl_api.process_flow_upload(flows) if flows else ""
return JSONResponse(content={
"message": f"{response_meters}, {response_groups}, {response_flows}".strip(", ")
})
@app.post("/flowdel")
async def post_flow_delete(entries: List[FlowEntry]):
"""Receive flows delete request"""
return ctrl_api.delete_flow_list([e.dict(exclude_unset=True) for e in entries])
@app.post("/flowmonitor")
async def post_flow_monitor(entries: List[FlowEntry]):
"""Receive flows monitor request"""
return ctrl_api.monitor_flow_list([e.dict(exclude_unset=True) for e in entries])
@app.post("/resetmonitor")
async def post_reset_flow_monitor(data: dict):
"""Reset flows monitoring data"""
return ctrl_api.rest_flow_monitoring(data)
@app.websocket("/ws")
async def websocket_handler(websocket: WebSocket):
await websocket.accept()
manager.register(websocket)
logger.debug("WebSocket connected: %s", websocket)
try:
while True:
# The JS client never sends meaningful data — this just keeps the
# connection open and detects clean disconnects via the exception.
await websocket.receive_text()
except WebSocketDisconnect:
logger.debug("WebSocket disconnected: %s", websocket)
finally:
manager.unregister(websocket)
# Mount static files AFTER all other routes to avoid shadowing
app.mount(
"/home",
StaticFiles(directory=os.path.join(os.path.dirname(__file__), "web"), html=True),
name="web",
)
def run_server(ctrl, host, port):
global ctrl_api
ctrl_api = ctrl
# Capture the event loop once uvicorn starts it, so broadcast_sync() can
# schedule coroutines onto it from the OS-Ken hub thread.
@app.on_event("startup")
async def _on_startup():
import asyncio
manager.set_loop(asyncio.get_running_loop())
app.state.manager = manager # also available via app.state if needed
import uvicorn
uvicorn.run(app, host=host, port=port, log_level="info")