From 0c238bdb6512c92e579d60f6ceebb4e13acdf085 Mon Sep 17 00:00:00 2001 From: "Dustin C. Hatch" Date: Sun, 15 Feb 2015 14:51:39 -0600 Subject: [PATCH] Initial commit --- .gitignore | 3 ++ debug.py | 19 +++++++++ setup.py | 16 ++++++++ src/dyns/__init__.py | 22 ++++++++++ src/dyns/controllers.py | 89 +++++++++++++++++++++++++++++++++++++++++ src/dyns/model.py | 52 ++++++++++++++++++++++++ src/dyns/routes.py | 8 ++++ 7 files changed, 209 insertions(+) create mode 100644 .gitignore create mode 100644 debug.py create mode 100644 setup.py create mode 100644 src/dyns/__init__.py create mode 100644 src/dyns/controllers.py create mode 100644 src/dyns/model.py create mode 100644 src/dyns/routes.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..14c71f0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +*.egg-info/ +__pycache__/ +*.py[co] diff --git a/debug.py b/debug.py new file mode 100644 index 0000000..04fe983 --- /dev/null +++ b/debug.py @@ -0,0 +1,19 @@ +from werkzeug import serving +import dyns +import os.path + + +app = dyns.make_app() + + +try: + serving.run_simple( + hostname='::', + port=8080, + application=app, + static_files={ + '/static': os.path.join(os.path.dirname(__file__), 'static'), + }, + ) +except KeyboardInterrupt: + print('Exiting...') diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..bd72750 --- /dev/null +++ b/setup.py @@ -0,0 +1,16 @@ +from setuptools import find_packages, setup + +setup( + name='DyNS', + version='0.1', + description='Dynamic Nameserver Configuration UI', + author='Dustin C. Hatch', + author_email='dustin@hatch.name', + url='https://bitbucket.org/AdmiralNemo/dyns', + license='GPL-3', + packages=find_packages('src'), + package_dir={'': 'src'}, + install_requires=[ + 'Milla', + ], +) diff --git a/src/dyns/__init__.py b/src/dyns/__init__.py new file mode 100644 index 0000000..c7eb7bb --- /dev/null +++ b/src/dyns/__init__.py @@ -0,0 +1,22 @@ +import sqlalchemy +import milla +import milla.util + + +DEFAULT_CONFIG = { + 'sqlalchemy.url': 'postgresql:///named', +} + + +def make_app(filename=None): + from . import routes, model + + app = milla.Application(routes.router) + app.config.update(DEFAULT_CONFIG) + if filename: + app.config.update(milla.util.read_config(filename)) + + engine = sqlalchemy.engine_from_config(app.config, 'sqlalchemy.') + model.Session.configure(bind=engine) + + return app diff --git a/src/dyns/controllers.py b/src/dyns/controllers.py new file mode 100644 index 0000000..6051389 --- /dev/null +++ b/src/dyns/controllers.py @@ -0,0 +1,89 @@ +from . import model +import json +import milla + + +@milla.allow('GET', 'HEAD', 'POST') +def all_zones(request): + response = request.ResponseClass() + response.content_type = 'application/json' + + session = model.Session() + if request.method == 'GET': + zones = map(model.Zone.as_dict, session.query(model.Zone)) + json.dump(list(zones), response.body_file) + session.rollback() + elif request.method == 'POST': + data = json.loads(request.text) + zone = model.Zone(**data) + session.add(zone) + session.commit() + response.status_int = 201 + return response + + +@milla.allow('GET', 'HEAD', 'POST', 'PUT', 'DELETE') +def zone(request, name): + response = request.ResponseClass() + response.content_type = 'application/json' + + session = model.Session() + zone = session.query(model.Zone).get(name) + if not zone: + raise milla.HTTPNotFound + + if request.method == 'GET': + zone_d = zone.as_dict() + zone_d['records'] = list(map(model.Record.as_dict, zone.records)) + json.dump(zone_d, response.body_file) + session.rollback() + elif request.method == 'PUT': + data = json.loads(request.text) + for k, v in data.items(): + assert k != 'records' + assert hasattr(zone, k) + setattr(zone, k, v) + session.commit() + elif request.method == 'POST': + data = json.loads(request.text) + zone_name = data.pop('zone', zone.name) + assert zone_name == zone.name + record = model.Record(zone=zone_name, **data) + session.add(record) + session.commit() + response.status_int = 201 + response.location = request.create_href_full( + '/records/{}'.format(record.id) + ) + elif request.method == 'DELETE': + session.delete(zone) + session.commit() + else: + session.rollback() + return response + +@milla.allow('GET', 'HEAD', 'PUT', 'DELETE') +def record(request, id): + response = request.ResponseClass() + response.content_type = 'application/json' + + session = model.Session() + record = session.query(model.Record).get(id) + if not record: + raise milla.HTTPNotFound + + if request.method == 'GET': + json.dump(record.as_dict(), response.body_file) + session.rollback() + elif request.method == 'DELETE': + session.delete(record) + session.commit() + elif request.method == 'PUT': + data =json.loads(request.text) + for k, v in data.items(): + assert hasattr(record, k) + setattr(record, k, v) + session.commit() + else: + session.rollback() + return response diff --git a/src/dyns/model.py b/src/dyns/model.py new file mode 100644 index 0000000..53d3246 --- /dev/null +++ b/src/dyns/model.py @@ -0,0 +1,52 @@ +from __future__ import unicode_literals +from sqlalchemy import schema, types, orm +from sqlalchemy.ext import declarative + + +Base = declarative.declarative_base() + +Session = orm.session.sessionmaker() + +NoResultFound = orm.exc.NoResultFound + + +class Serializable(object): + + def as_dict(self): + def serialize(obj): + for col in obj.__table__.columns: + value = getattr(obj, col.name) + if isinstance(value, Serializable): + value = dict(serialize(value)) + yield (col.name, value) + return dict(serialize(self)) + + +class Zone(Base, Serializable): + + __tablename__ = 'zones' + + name = schema.Column(types.Unicode(254), primary_key=True) + ttl = schema.Column(types.Integer, nullable=False, default=3600) + source = schema.Column(types.Unicode(254), nullable=False) + contact = schema.Column(types.Unicode(254), nullable=False) + serial = schema.Column(types.Integer, nullable=False, default=1024) + refresh = schema.Column(types.Integer, nullable=False, default=900) + retry = schema.Column(types.Integer, nullable=False, default=600) + expire = schema.Column(types.Integer, nullable=False, default=86400) + minimum = schema.Column(types.Integer, nullable=False, default=3600) + records = orm.relationship('Record', cascade='delete') + + +class Record(Base, Serializable): + + __tablename__ = 'records' + + id = schema.Column(types.Integer, autoincrement=True, primary_key=True) + zone = schema.Column(types.Unicode(254), schema.ForeignKey(Zone.name), + nullable=False) + host = schema.Column(types.Unicode(254), nullable=False) + ttl = schema.Column(types.Integer, nullable=False, default=3600) + rdtype = schema.Column(types.Unicode(9), nullable=False) + mx_prio = schema.Column(types.Integer) + data = schema.Column(types.UnicodeText) diff --git a/src/dyns/routes.py b/src/dyns/routes.py new file mode 100644 index 0000000..fea3ca7 --- /dev/null +++ b/src/dyns/routes.py @@ -0,0 +1,8 @@ +from . import controllers +from milla.dispatch import routing + +router = routing.Router() + +router.add_route('/zones/', controllers.all_zones) +router.add_route('/zones/{name}', controllers.zone) +router.add_route('/records/{id}', controllers.record)