Refactor controller to HTTP REST API
This commit is contained in:
parent
6d016d272f
commit
b820eaff52
@ -1,58 +1,46 @@
|
||||
# FieldPOC controller
|
||||
|
||||
The FieldPOC controller is an interactive console to help managing the current state of the FieldPOC system.
|
||||
The FieldPOC controller is a HTTP REST API to help managing the current state of the FieldPOC system.
|
||||
|
||||
Connect to it via the IP address von port set in the [FieldPOC configuration](configuration.md) with `telnet`.
|
||||
Connect to it via the IP address von port set in the [FieldPOC configuration](configuration.md).
|
||||
|
||||
## Commands
|
||||
## API Endpoints
|
||||
|
||||
### Help
|
||||
### `/sync`
|
||||
|
||||
`help`
|
||||
|
||||
Show help info.
|
||||
|
||||
### Show handlers
|
||||
|
||||
`handlers`
|
||||
|
||||
Show currently running handlers
|
||||
|
||||
### Reconfigure all components
|
||||
|
||||
`sync`
|
||||
```bash
|
||||
curl --json '{}' http://127.0.0.1:9437/sync
|
||||
```
|
||||
|
||||
Notify all parts of FieldPOC to check configuration of connected components and update it.
|
||||
|
||||
### Show queue stats
|
||||
### `/queues`
|
||||
|
||||
`queues`
|
||||
```bash
|
||||
curl http://127.0.0.1:9437/queues
|
||||
```
|
||||
|
||||
### Reload configuration
|
||||
Show internal message queue stats.
|
||||
|
||||
`reload`
|
||||
### `/reload`
|
||||
|
||||
Read [FieldPOC configuration](configuration.md) file and apply it.
|
||||
```bash
|
||||
curl --json '{}' http://127.0.0.1:9437/reload
|
||||
```
|
||||
|
||||
### Bind extension to DECT device
|
||||
Read [FieldPOC extensions specification](extensions.md) file and apply it.
|
||||
|
||||
`claim <ext> <token>`
|
||||
### `/claim`
|
||||
|
||||
- `ext` is the current extension number of the DECT device.
|
||||
```bash
|
||||
curl --json '{"extension": "1111", "token": "1234"}' http://127.0.0.1:9437/claim
|
||||
```
|
||||
|
||||
Bind extension to DECT device
|
||||
|
||||
- `extension` is the current extension number of the DECT device.
|
||||
- `token` is the `dect_claim_token` of the extension that should get applied.
|
||||
|
||||
This works because newly connected DECT phones get a temporary number assigned.
|
||||
This temporary number is usually the current number.
|
||||
But it is possible to use any extension, so the extension for a device can be changed any time.
|
||||
|
||||
### Disconnect
|
||||
|
||||
`exit`
|
||||
|
||||
Disconnect telnet session.
|
||||
|
||||
### Stop controller
|
||||
|
||||
`stop`
|
||||
|
||||
Shutdown the controller, but FieldPOC continues running.
|
||||
|
@ -1,98 +1,190 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import logging
|
||||
import selectors
|
||||
import socket
|
||||
import socketserver
|
||||
import time
|
||||
from http.server import ThreadingHTTPServer, BaseHTTPRequestHandler, HTTPStatus
|
||||
import threading
|
||||
import json
|
||||
|
||||
logger = logging.getLogger("fieldpoc.controller")
|
||||
|
||||
class Controller:
|
||||
def __init__(self, fp):
|
||||
self.fp = fp
|
||||
self.handlers = [] # collect control sockets for handlers, so we can shut them down on demand
|
||||
|
||||
def get_handler(self):
|
||||
class HandleRequest(socketserver.BaseRequestHandler):
|
||||
fp = self.fp
|
||||
controller = self
|
||||
"""
|
||||
Create a handler passed to the HTTP server
|
||||
|
||||
def setup(self):
|
||||
self.handler_read, self.handler_write = socket.socketpair()
|
||||
self.controller.handlers.append(self.handler_write)
|
||||
Return a subclass of BaseHTTPRequestHandler
|
||||
"""
|
||||
|
||||
def handle(self):
|
||||
self.request.sendall("FieldPOC interactive controller\n".encode("utf-8"))
|
||||
# Paths by HTTP request method
|
||||
PATHS = {
|
||||
"GET": {},
|
||||
"POST": {},
|
||||
}
|
||||
|
||||
sel = selectors.DefaultSelector()
|
||||
sel.register(self.handler_read, selectors.EVENT_READ)
|
||||
sel.register(self.request, selectors.EVENT_READ)
|
||||
while True:
|
||||
self.request.sendall("> ".encode("utf-8"))
|
||||
def index():
|
||||
"""
|
||||
Index page
|
||||
"""
|
||||
|
||||
for key, mask in sel.select():
|
||||
if key.fileobj == self.handler_read:
|
||||
self.request.sendall("\ndisconnecting\n".encode("utf-8"))
|
||||
return
|
||||
elif key.fileobj == self.request:
|
||||
data = self.request.recv(1024).decode("utf-8").strip()
|
||||
return "FieldPOC HTTP Controller"
|
||||
|
||||
PATHS["GET"]["/"] = index
|
||||
|
||||
def sync(payload):
|
||||
"""
|
||||
Notify all parts of FieldPOC to check configuration of connected components and update it
|
||||
"""
|
||||
|
||||
if data == "help":
|
||||
self.request.sendall("""Availiable commands:
|
||||
help Show this info
|
||||
handlers Show currently running handlers
|
||||
sync Start syncing
|
||||
queues Show queue stats
|
||||
reload Reload extension config file
|
||||
claim <ext> <token> claim dect extension
|
||||
exit Disconnect
|
||||
""".encode("utf-8"))
|
||||
elif data == "":
|
||||
continue
|
||||
elif data == "quit" or data == "exit":
|
||||
self.request.sendall("disconnecting\n".encode("utf-8"))
|
||||
return
|
||||
elif data == "handlers":
|
||||
self.request.sendall(("\n".join([str(h) for h in self.controller.handlers]) + "\n").encode("utf-8"))
|
||||
elif data == "sync":
|
||||
self.fp.queue_all({"type": "sync"})
|
||||
elif data == "queues":
|
||||
self.request.sendall(("\n".join(["{} {}".format(name, queue.qsize()) for name, queue in self.fp.queues.items()]) + "\n").encode("utf-8"))
|
||||
elif data == "reload":
|
||||
self.fp.reload_extensions()
|
||||
elif data.startswith("claim"):
|
||||
data = data.split(" ")
|
||||
if len(data) == 3:
|
||||
self.fp.queue_all({"type": "claim", "extension": data[1], "token": data[2]})
|
||||
else:
|
||||
self.request.sendall("error: You have to specify calling extension and token\n".encode("utf-8"))
|
||||
else:
|
||||
self.request.sendall("Unknown command, type 'help'\n".encode("utf-8"))
|
||||
|
||||
def finish(self):
|
||||
self.controller.handlers.remove(self.handler_write)
|
||||
return "ok"
|
||||
|
||||
PATHS["POST"]["/sync"] = sync
|
||||
|
||||
def queues():
|
||||
"""
|
||||
Show status of message queues
|
||||
"""
|
||||
|
||||
return {name: queue.qsize() for name, queue in self.fp.queues.items()}
|
||||
|
||||
PATHS["GET"]["/queues"] = queues
|
||||
|
||||
def reload(payload):
|
||||
"""
|
||||
Read extensions specification and apply it
|
||||
"""
|
||||
|
||||
self.fp.reload_extensions()
|
||||
|
||||
PATHS["POST"]["/reload"] = reload
|
||||
|
||||
def claim(payload):
|
||||
"""
|
||||
Bind extension to DECT device
|
||||
"""
|
||||
|
||||
if payload is None:
|
||||
return {}
|
||||
try:
|
||||
self.fp.queue_all({"type": "claim", "extension": payload["extension"], "token": payload["token"]})
|
||||
return "ok"
|
||||
except KeyError:
|
||||
return { "error": "You have to specify calling extension and token" }
|
||||
|
||||
PATHS["POST"]["/claim"] = claim
|
||||
|
||||
class HandleRequest(BaseHTTPRequestHandler):
|
||||
|
||||
def do_GET(self):
|
||||
if self.path in PATHS["GET"]:
|
||||
response = PATHS["GET"][self.path]()
|
||||
return self.make_response(response)
|
||||
else:
|
||||
self.send_error(HTTPStatus.NOT_FOUND, "Not found")
|
||||
return
|
||||
|
||||
def do_HEAD(self):
|
||||
if self.path in PATHS["GET"]:
|
||||
response = PATHS["GET"][self.path]()
|
||||
return self.make_response(response, head_only=True)
|
||||
else:
|
||||
self.send_error(HTTPStatus.NOT_FOUND, "Not found")
|
||||
return
|
||||
|
||||
def do_POST(self):
|
||||
if self.path in PATHS["POST"]:
|
||||
payload = self.prepare_payload()
|
||||
response = PATHS["POST"][self.path](payload)
|
||||
return self.make_response(response)
|
||||
else:
|
||||
self.send_error(HTTPStatus.NOT_FOUND, "Not found")
|
||||
return
|
||||
|
||||
def make_response(self, content, head_only=False):
|
||||
"""
|
||||
Take a dict and return as HTTP json response
|
||||
"""
|
||||
|
||||
# Convert response to json
|
||||
content = json.dumps(content) + "\n"
|
||||
# Encode repose as binary
|
||||
encoded = content.encode("utf-8")
|
||||
# Send 200 status code
|
||||
self.send_response(HTTPStatus.OK)
|
||||
# Set json header
|
||||
self.send_header("Content-Type", "application/json; charset=utf-8")
|
||||
# Specify content length
|
||||
self.send_header("Conten-Length", str(len(encoded)))
|
||||
# Finish headers
|
||||
self.end_headers()
|
||||
|
||||
# Return early on HEAD request
|
||||
if head_only:
|
||||
return
|
||||
|
||||
# Send response
|
||||
self.wfile.write(encoded)
|
||||
|
||||
def prepare_payload(self):
|
||||
"""
|
||||
Read payload data from HTTP POST request and parse as json.
|
||||
"""
|
||||
|
||||
# Get payload length
|
||||
length = int(self.headers["Content-Length"])
|
||||
# Read payload up to length
|
||||
encoded = self.rfile.read(length)
|
||||
# Decode payload to string
|
||||
content = encoded.decode("utf-8")
|
||||
|
||||
# Return early if no payload
|
||||
if not content:
|
||||
return None
|
||||
|
||||
# Parse payload as json
|
||||
return json.loads(content)
|
||||
|
||||
|
||||
return HandleRequest
|
||||
|
||||
|
||||
def run(self):
|
||||
logger.info("starting server")
|
||||
class Server(socketserver.ThreadingTCPServer):
|
||||
|
||||
class Server(ThreadingHTTPServer):
|
||||
"""
|
||||
Subclass ThreadingHTTPServer to set custom options
|
||||
"""
|
||||
|
||||
# Sometimes sockets are still bound to addresses and ports
|
||||
# we just ignore that with this
|
||||
allow_reuse_address = True
|
||||
|
||||
with Server((self.fp.config.controller.host, self.fp.config.controller.port), self.get_handler()) as server:
|
||||
# Start the server
|
||||
with Server(
|
||||
# Passing address and port from config
|
||||
(self.fp.config.controller.host, self.fp.config.controller.port),
|
||||
# With the handler class we generated
|
||||
self.get_handler()
|
||||
) as server:
|
||||
|
||||
# Starting server loop in another thread
|
||||
threading.Thread(target=server.serve_forever).start()
|
||||
|
||||
# Listen on the message queue
|
||||
while True:
|
||||
# Wait for a new message and get it
|
||||
msg = self.fp.queues["controller"].get()
|
||||
# Remove message from queue
|
||||
self.fp.queues["controller"].task_done()
|
||||
|
||||
if msg.get("type") == "stop":
|
||||
logger.info("stopping server")
|
||||
# Stop http server loop
|
||||
server.shutdown()
|
||||
for h in self.handlers:
|
||||
h.send(b'\0')
|
||||
# Exist message queue listening loop
|
||||
break
|
||||
|
Loading…
Reference in New Issue
Block a user