1
|
|
|
"""Module used to handle a API Server.""" |
2
|
1 |
|
import logging |
3
|
|
|
import os |
4
|
1 |
|
import sys |
5
|
1 |
|
from urllib.error import URLError |
6
|
|
|
from urllib.request import urlopen |
7
|
1 |
|
|
8
|
1 |
|
from flask import Flask, request, send_from_directory |
9
|
|
|
from flask_socketio import SocketIO, join_room, leave_room |
10
|
|
|
|
11
|
1 |
|
|
12
|
|
|
class APIServer: |
13
|
|
|
"""Api server used to provide Kytos Controller routes.""" |
14
|
1 |
|
|
15
|
|
|
def __init__(self, app_name, listen='0.0.0.0', port=8181): |
16
|
|
|
"""Constructor of APIServer. |
17
|
|
|
|
18
|
|
|
This method will instantiate a server with SocketIO+Flask. |
19
|
|
|
|
20
|
|
|
Parameters: |
21
|
|
|
app_name(string): String representing a App Name |
22
|
|
|
listen (string): host name used by api server instance |
23
|
|
|
port (int): Port number used by api server instance |
24
|
|
|
""" |
25
|
|
|
dirname = os.path.dirname(os.path.abspath(__file__)) |
26
|
|
|
self.flask_dir = os.path.join(dirname, '../web-ui') |
27
|
|
|
self.log = logging.getLogger('werkzeug') |
28
|
1 |
|
|
29
|
|
|
self.listen = listen |
30
|
|
|
self.port = port |
31
|
|
|
|
32
|
1 |
|
self.app = Flask(app_name, root_path=self.flask_dir) |
33
|
|
|
self.server = SocketIO(self.app, async_mode='threading') |
34
|
|
|
self._enable_websocket_rooms() |
35
|
|
|
|
36
|
|
|
def _enable_websocket_rooms(self): |
37
|
|
|
socket = self.server |
38
|
|
|
socket.on_event('join', join_room) |
39
|
|
|
socket.on_event('leave', leave_room) |
40
|
|
|
|
41
|
|
|
def run(self): |
42
|
|
|
"""Run the Flask API Server.""" |
43
|
|
|
try: |
44
|
|
|
self.server.run(self.app, self.listen, self.port) |
45
|
|
|
except OSError as e: |
46
|
|
|
msg = "Couldn't start API Server: {}".format(e) |
47
|
|
|
self.log.critical(msg) |
48
|
|
|
sys.exit(msg) |
49
|
1 |
|
|
50
|
|
|
def register_rest_endpoint(self, url, function, methods): |
51
|
|
|
r"""Register a new rest endpoint in Api Server. |
52
|
|
|
|
53
|
|
|
To register new endpoints is needed to have a url, function to handle |
54
|
|
|
the requests and type of method allowed. |
55
|
1 |
|
|
56
|
|
|
Parameters: |
57
|
|
|
url (string): String with partner of route. e.g.: '/status' |
58
|
|
|
function (function): Function pointer used to handle the requests. |
59
|
1 |
|
methods (list): List of request methods allowed. |
60
|
|
|
e.g: ['GET', 'PUT', 'POST', 'DELETE', 'PATCH'] |
61
|
|
|
""" |
62
|
|
|
new_endpoint_url = "/kytos{}".format(url) |
63
|
|
|
|
64
|
1 |
|
for endpoint in self.app.url_map.iter_rules(): |
65
|
|
|
if endpoint.rule == new_endpoint_url: |
66
|
|
|
for method in methods: |
67
|
|
|
if method in endpoint.methods: |
68
|
|
|
message = ("Method '{}' already registered for " + |
69
|
1 |
|
"URL '{}'").format(method, new_endpoint_url) |
70
|
|
|
self.log.warning(message) |
71
|
|
|
self.log.warning("WARNING: Overlapping endpoint was " + |
72
|
|
|
"NOT registered.") |
73
|
|
|
return |
74
|
|
|
|
75
|
|
|
self.app.add_url_rule(new_endpoint_url, function.__name__, function, |
76
|
|
|
methods=methods) |
77
|
|
|
|
78
|
|
|
def register_web_ui(self): |
79
|
|
|
"""Method used to register routes to the admin-ui homepage.""" |
80
|
|
|
self.app.add_url_rule('/', self.web_ui.__name__, self.web_ui) |
81
|
|
|
self.app.add_url_rule('/index.html', self.web_ui.__name__, self.web_ui) |
82
|
|
|
|
83
|
1 |
|
@property |
84
|
|
|
def rest_endpoints(self): |
85
|
|
|
"""Return string with routes registered by Api Server.""" |
86
|
|
|
return [x.rule for x in self.app.url_map.iter_rules()] |
87
|
1 |
|
|
88
|
|
|
def register_api_server_routes(self): |
89
|
|
|
"""Register initial routes from kytos using ApiServer. |
90
|
|
|
|
91
|
|
|
Initial routes are: ['/kytos/status/', '/kytos/shutdown/'] |
92
|
|
|
""" |
93
|
|
|
if '/kytos/status/' not in self.rest_endpoints: |
94
|
1 |
|
self.register_rest_endpoint('/status/', |
95
|
|
|
self.status_api, methods=['GET']) |
96
|
|
|
|
97
|
|
|
if '/kytos/shutdown/' not in self.rest_endpoints: |
98
|
|
|
self.register_rest_endpoint('/shutdown/', |
99
|
|
|
self.shutdown_api, methods=['GET']) |
100
|
|
|
|
101
|
|
|
@staticmethod |
102
|
|
|
def status_api(): |
103
|
|
|
"""Display json with kytos status using the route '/kytos/status/'.""" |
104
|
|
|
return '{"response": "running"}', 201 |
105
|
|
|
|
106
|
|
|
def stop_api_server(self): |
107
|
|
|
"""Method used to send a shutdown request to stop Api Server.""" |
108
|
1 |
|
try: |
109
|
|
|
url = 'http://{}:{}/kytos/shutdown'.format('127.0.0.1', self.port) |
110
|
|
|
urlopen(url) |
111
|
|
|
except URLError: |
112
|
|
|
pass |
113
|
|
|
|
114
|
|
|
def shutdown_api(self): |
115
|
|
|
"""Handle shutdown requests received by Api Server. |
116
|
|
|
|
117
|
|
|
This method must be called by kytos using the method |
118
|
|
|
stop_api_server, otherwise this request will be ignored. |
119
|
|
|
""" |
120
|
|
|
allowed_host = ['127.0.0.1:'+str(self.port), |
121
|
|
|
'localhost:'+str(self.port)] |
122
|
|
|
if request.host not in allowed_host: |
123
|
|
|
return "", 403 |
124
|
|
|
|
125
|
|
|
self.server.stop() |
126
|
|
|
|
127
|
|
|
return 'Server shutting down...', 200 |
128
|
|
|
|
129
|
|
|
def web_ui(self): |
130
|
|
|
"""Method used to serve the index.html page for the admin-ui.""" |
131
|
|
|
return send_from_directory(self.flask_dir, 'index.html') |
132
|
|
|
|