Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
*.py[co]
__pycache__/
venv/
config.py
.*
!.git*
21 changes: 20 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,24 @@ Setup
- fish: `. venv/bin/activate.fish`
- csh, tcsh: `source venv/bin/activate.csh`
3. Install dependencies if necessary: `pip install -r requirements.txt`
4. configure: `cp config.py{.example,}; $EDITOR config.py`
5. bootstrap (create DB tables and such): `./bootstrap.py`
6. run with `./runserver.py`;
run the HTTP API server with `./runhttp.py`

The rest doesn't exist yet.
Running tests
-------------

1. Edit configuration: `cp tests/config.py{.example,}; $EDITOR tests/config.py`
A real Postgres DB is used, you need to specify the connection string.
2. run with `py.test tests/`
or `py.test --cov gateserver/ --cov-report term-missing tests/` for coverage report

Next to do:
-----------

- the controller end
- wrap/unwrap NaCl
- HTTP: rewrite to use Werkzeug instead of CherryPy
- fix DB singleton (who wants a singleton?!)
- CI
19 changes: 19 additions & 0 deletions bootstrap.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#!/usr/bin/env python3

import config
from gateserver import db

def db_create_tables():
print('Creating DB tables...', end='')
with open('./tables.txt', 'r') as f:
if not db.conn: db.connect(config.db_url)
for line in f:
line = line.split('#')[0].strip()
if line: db.exec_sql('CREATE TABLE IF NOT EXISTS ' + line)
print(' OK')

def all():
db_create_tables()

if __name__ == '__main__':
all()
5 changes: 5 additions & 0 deletions config.py.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
"""The server configuration."""

http_port = 5047
udp_port = 5042
db_url = 'postgresql://user:password@localhost/gate'
Empty file added gateserver/__init__.py
Empty file.
24 changes: 24 additions & 0 deletions gateserver/controller_server.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
"""The UDP server that provides the API for the controllers."""

#from . import controller_api
import socketserver
import logging

log = logging.getLogger('server')

class MessageHandler(socketserver.BaseRequestHandler):
"""Handles a message from the controller.

Behaves according to
https://github.com/fmfi-svt/gate/wiki/Controller-%E2%86%94-Server-Protocol .
"""

def handle(self):
data, socket = self.request
socket.sendto(data, self.client_address)
log.info(data, extra=dict(ip=self.client_address[0], status='OK'))

def serve(config):
bind_addr = '0.0.0.0', config.udp_port
server = socketserver.ThreadingUDPServer(bind_addr, MessageHandler)
server.serve_forever()
21 changes: 21 additions & 0 deletions gateserver/db.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
"""Holds the (global) connection to the DB."""
# TODO maybe use a connection pool one beautiful day

import psycopg2
from psycopg2.extras import RealDictCursor # results as dict instead of tuple

conn = None

def connect(db_url):
global conn
conn = psycopg2.connect(db_url, cursor_factory=RealDictCursor)
conn.autocommit = True

def exec_sql(query, args=(), ret=False):
"""Execute the query, returning the result as a list if `ret` is set."""
with conn.cursor() as cur:
cur.execute(query, args)
if ret: return cur.fetchall()

# thrown when constraints aren't satisfied
IntegrityError = psycopg2.IntegrityError
87 changes: 87 additions & 0 deletions gateserver/http_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
"""Defines the REST API for CRUD and management."""

from . import db
import nacl.raw as nacl
import cherrypy

class MountPoint:
"""Represents a mount point, or path prefix, for attaching resources to."""

class Resource(MountPoint):
"""Represents a REST resource."""
exposed = True

class Log(Resource):
@cherrypy.tools.json_out()
def GET(self, entries=100):
return db.exec_sql('SELECT * FROM log ORDER BY time DESC LIMIT %s',
(entries,), ret=True)

class CRUDResource(Resource):
"""Represents a REST resource that exposes a DB table's CRUD methods."""
def __init__(self, tbl, put_columns, get_columns, on_save=lambda x: x):
assert(tbl.isidentifier())
self.table = tbl
self.put_columns = put_columns
self.get_columns = get_columns
self.on_save = on_save

@cherrypy.tools.json_out()
def GET(self, id=None):
cols = list(self.get_columns)
q = 'SELECT {} FROM {}'.format(','.join(cols), self.table)
if id: q += ' WHERE id = %s'
rs = db.exec_sql(q, (id,), ret=True)
if id:
if len(rs) < 1: raise cherrypy.HTTPError('404 Not Found')
return rs[0]
else: return rs

@cherrypy.tools.json_in()
@cherrypy.tools.json_out()
def PUT(self, id):
json = self.on_save(dict(cherrypy.request.json, id=id))
print(json)
cols, values, ps = [], [], []
for c in self.put_columns:
cols.append(c)
values.append(json.get(c))
ps.append('%s')
q = 'INSERT INTO controller ({}) VALUES ({})'.format(','.join(cols),
','.join(ps))
try:
db.exec_sql(q, values)
except db.IntegrityError as e:
raise cherrypy.HTTPError('400 Bad Request', e.pgerror) from e
return { 'url': cherrypy.url() }

# TODO POST

def DELETE(self, id):
db.exec_sql('DELETE FROM {} WHERE id = %s'.format(self.table), (id,))

################################################################################

api_root = MountPoint()
api_root.controller = CRUDResource('controller',
get_columns={'id', 'ip', 'name'},
put_columns={'id', 'ip', 'key', 'name'},
on_save=lambda ctrl:
dict(ctrl, key=nacl.randombytes(nacl.crypto_secretbox_KEYBYTES)))

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, detaily tabuliek by som cakal v inom subore nez vseobecne triedy vyssie.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Toto chce byť o tom, že "čo vidno v HTTP API". (Skutočný obsah tabuliek sú nadmnožiny toho, čo je tuto.) V jednom súbore je to preto, že všetko (vrátane tých všeobecných tried) to definuje, ako vyzerá HTTP API. Ale som ukecateľná na rozdelenie. A vlastne to aj tak idem celé prepísať... :D

api_root.log = Log()

################################################################################

cherrypy_conf = {
'/': {
'request.dispatch': cherrypy.dispatch.MethodDispatcher(),
}
}
cherrypy.tree.mount(api_root, '/', cherrypy_conf)

def serve(config):
cherrypy.config.update({'server.socket_port': config.http_port})
cherrypy.engine.start()

def stop():
cherrypy.engine.exit()
7 changes: 7 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,9 @@
CherryPy==3.6.0
cov-core==1.15.0
coverage==3.7.1
psycopg2==2.5.4
py==1.4.26
pytest==2.6.4
pytest-cov==1.8.1
requests==2.5.1
https://github.com/warner/python-tweetnacl/tarball/b48a25a33f
9 changes: 9 additions & 0 deletions runhttp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#!/usr/bin/env python3
"""Gate HTTP API server runner."""

from gateserver import db, http_api
import config

if __name__ == '__main__':
db.connect(config.db_url)
http_api.serve(config)
17 changes: 17 additions & 0 deletions runserver.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#!/usr/bin/env python3
"""Gate server runner."""

from gateserver import db, controller_server
import config
import logging

logging.basicConfig(level=logging.DEBUG,
format='%(asctime)s %(levelname)s %(name)s %(ip)s %(message)s %(status)s',
datefmt='%Y-%m-%d %H:%M:%S')

if __name__ == '__main__':
try:
db.connect(config.db_url)
controller_server.serve(config)
except (SystemExit, KeyboardInterrupt):
logging.shutdown()
4 changes: 4 additions & 0 deletions tables.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# SQL (CREATE TABLE) syntax (these are executed during bootstrap)
# one line per table
controller (id macaddr PRIMARY KEY, ip inet UNIQUE NOT NULL, key bytea NOT NULL, name text)
log (time timestamp NOT NULL, ctrl_id macaddr REFERENCES controller, message text)
Empty file added tests/__init__.py
Empty file.
5 changes: 5 additions & 0 deletions tests/config.py.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
"""Tests configuration."""

http_port = 9047
udp_port = 9042
db_url = 'postgresql://user:password@localhost/gate_test'
40 changes: 40 additions & 0 deletions tests/test_http_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import tests.config as config
import bootstrap
import gateserver.db
import gateserver.http_api
import requests

url = 'http://localhost:{}'.format(config.http_port)

def setup_module(module):
gateserver.db.connect(config.db_url)
bootstrap.db_create_tables()
gateserver.http_api.serve(config)

def teardown_module(module):
gateserver.http_api.stop()

def assert_req(method, url, status=200, expected_data=None, **kwargs):
print(kwargs)
r = requests.request(method, url, **kwargs)
assert r.status_code == status
if expected_data: assert r.json() == expected_data

def test_controller_crud():
cid = '00:00:00:00:00:00'
data = { 'id': cid, 'ip': '0.0.0.0', 'name': 'Test Controller' }
requests.delete(url+'/controller/'+cid) # just in case the last run didn't

assert_req('PUT', url+'/controller/'+cid, json={'ip': data['ip'],
'name': data['name']},
expected_data={'url': url+'/controller/'+cid})
assert_req('GET', url+'/controller/'+cid, expected_data=data)
assert_req('GET', url+'/controller/', expected_data=[data])
assert_req('PUT', url+'/controller/'+cid, json={}, status=400)
assert_req('DELETE', url+'/controller/'+cid)
assert_req('GET', url+'/controller/'+cid, status=404)

def test_log():
r = requests.get(url+'/log/')
assert r.status_code == 200
assert isinstance(r.json(), list)