Skip to content

Commit e9f5f64

Browse files
committed
added first version of an HTTP REST API
1 parent ccb7252 commit e9f5f64

File tree

8 files changed

+141
-2
lines changed

8 files changed

+141
-2
lines changed

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ kubernetes
77
xdg
88
dotmap
99
PyYAML
10+
tornado

setup.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@
3131
'villas-node>=0.10.2',
3232
'kubernetes',
3333
'xdg',
34-
'PyYAML'
34+
'PyYAML',
35+
'tornado'
3536
],
3637
data_files=[
3738
('/etc/villas/controller', glob('etc/*.{json,yaml}')),

villas/controller/api.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import threading
2+
import logging
3+
import asyncio
4+
from tornado.ioloop import IOLoop
5+
import tornado.web
6+
7+
LOGGER = logging.getLogger(__name__)
8+
REGEX_UUID = r'\b[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}-[0-9a-fA-F]' \
9+
r'{4}-[0-9a-fA-F]{4}-\b[0-9a-fA-F]{12}\b'
10+
11+
12+
class RequestHandler(tornado.web.RequestHandler):
13+
14+
def initialize(self, controller):
15+
self.controller = controller
16+
17+
18+
class Api(threading.Thread):
19+
20+
def __init__(self, controller):
21+
super().__init__()
22+
23+
self.controller = controller
24+
25+
def run(self):
26+
self.app = tornado.web.Application(self.handlers)
27+
28+
# Create new event loop for this thread
29+
aio_loop = asyncio.new_event_loop()
30+
asyncio.set_event_loop(aio_loop)
31+
32+
port = self.controller.config.api.port
33+
self.app.listen(port)
34+
35+
LOGGER.info('Starting API')
36+
37+
self.loop = IOLoop.current(instance=True)
38+
self.loop.start()
39+
40+
@property
41+
def handlers(self):
42+
from villas.controller.handlers.component import ComponentRequestHandler # noqa E501
43+
from villas.controller.handlers.main import MainRequestHandler
44+
from villas.controller.handlers.health import HealthRequestHandler
45+
46+
args = {
47+
'controller': self.controller
48+
}
49+
50+
return [
51+
(r'/', MainRequestHandler, args),
52+
(r'/health', HealthRequestHandler, args),
53+
(r'/component/('+REGEX_UUID+r')', ComponentRequestHandler, args)
54+
]

villas/controller/component.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,10 @@ def on_message(self, message):
112112
self.logger.debug('Received message: %s', message.payload)
113113

114114
if 'action' in message.payload:
115-
self.run_action(message.payload['action'], message.payload)
115+
try:
116+
self.run_action(message.payload['action'], message.payload)
117+
except SimulationException:
118+
pass
116119

117120
message.ack()
118121

@@ -151,6 +154,8 @@ def run_action(self, action, payload):
151154
self.logger.error('SimulationException: %s', str(se))
152155
self.change_state('error', msg=se.msg, **se.info)
153156

157+
raise se
158+
154159
def change_state(self, state, **kwargs):
155160
if self._state == state:
156161
return

villas/controller/controller.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import os
66
import kombu.mixins
77

8+
from villas.controller.api import Api
89
from villas.controller import __version__ as version
910
from villas.controller.components.managers.generic import GenericManager
1011

@@ -110,6 +111,11 @@ def on_iteration(self):
110111

111112
def start(self):
112113
self.started = time.time()
114+
115+
if self.config.api.enabled:
116+
self.api = Api(self)
117+
self.api.start()
118+
113119
self.should_terminate = False
114120
while not self.should_terminate:
115121
self.should_stop = False
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import json
2+
from tornado.web import HTTPError
3+
from http import HTTPStatus
4+
from functools import wraps
5+
6+
7+
from villas.controller.api import RequestHandler
8+
from villas.controller.exceptions import SimulationException
9+
10+
11+
def with_component(f):
12+
13+
@wraps(f)
14+
def wrapper(self, uuid):
15+
try:
16+
component = self.controller.components[uuid]
17+
18+
f(self, component)
19+
except KeyError:
20+
raise HTTPError(HTTPStatus.NOT_FOUND)
21+
22+
return wrapper
23+
24+
25+
class ComponentRequestHandler(RequestHandler):
26+
27+
@with_component
28+
def get(self, component):
29+
self.write(component.status)
30+
31+
@with_component
32+
def post(self, component):
33+
payload = json.loads(self.request.body)
34+
35+
action = payload.get('action')
36+
if action is None:
37+
raise HTTPError(HTTPStatus.BAD_REQUEST)
38+
39+
try:
40+
component.run_action(action, payload)
41+
self.write(component.status)
42+
43+
except SimulationException as se:
44+
self.write({
45+
'exception': {
46+
'msg': se.msg,
47+
'args': se.args
48+
},
49+
**component.status
50+
})
51+
self.send_error(HTTPStatus.BAD_REQUEST)

villas/controller/handlers/health.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
from villas.controller.api import RequestHandler
2+
3+
4+
class HealthRequestHandler(RequestHandler):
5+
6+
def get(self):
7+
self.write({
8+
'status': 'ok'
9+
})

villas/controller/handlers/main.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
from villas.controller.api import RequestHandler
2+
3+
4+
class MainRequestHandler(RequestHandler):
5+
6+
def get(self):
7+
self.write({
8+
'components': list(self.controller.components.keys()),
9+
'status': {
10+
**self.controller.status
11+
}
12+
})

0 commit comments

Comments
 (0)