Merge branch 'master' of github.com:managedit/moniker

This commit is contained in:
Kiall Mac Innes 2012-10-14 20:58:44 +01:00
commit bd58c9805b
33 changed files with 1394 additions and 119 deletions

2
.gitignore vendored
View File

@ -7,9 +7,11 @@ build
.tox .tox
cover cover
venv venv
.venv
*.sublime-workspace *.sublime-workspace
*.sqlite *.sqlite
var/* var/*
etc/*.conf etc/*.conf
etc/*.ini
AUTHORS AUTHORS
ChangeLog ChangeLog

View File

@ -1,5 +1,40 @@
# Introduction
TODOs: Moniker is an OpenStack inspired DNSaaS.
# Developer Guide:
NOTE: This is probably incomplete!
## Install Dependencies
1. `apt-get install python-pip python-virtualenv python-setuptools-git`
1. `apt-get install rabbitmq-server bind9`
1. `apt-get build-dep python-lxml`
## Install Moniker
1. `virtualenv .venv`
1. `source .venv/bin/activate`
1. `python setup.py develop`
1. create config files (See `*.sample` in the `etc` folder)
1. Ensure the user you intend to run moniker as has passwordless sudo rights:
* `echo "$USER ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/90-moniker-$USER`
* `chmod 0440 /etc/sudoers.d/90-moniker-$USER`
1. Tell bind to load our zones:
* Open `/etc/bind/named.conf`
* Add `include "$CHECKOUT_PATH/var/bind9/zones.config";` to the end of the file
* `sudo service bind9 restart`
## Run
1. Open 3 consoles/screen sessions for each command:
* `./bin/moniker-api`
* `./bin/moniker-central`
* `./bin/moniker-agent-bind9`
1. Make use of the API..
# TODOs:
* Documentation! * Documentation!
* Fixup Bind9 agent implementation so it could be considered even remotely reliable * Fixup Bind9 agent implementation so it could be considered even remotely reliable
@ -7,4 +42,9 @@ TODOs:
* Database migrations * Database migrations
* Unit Tests!! * Unit Tests!!
* Integration with other OS servers eg Nova and Quantum * Integration with other OS servers eg Nova and Quantum
* Introduce Server Groups * Listen for floating IP allocation/deallocation events - giving user access to
the necessary PTR record.
* Listen for server create/destroy events - creating DNS records as needed.
* Listen for server add/remove from security group events - creating "load balancing" DNS RR records as needed.
* Introduce Server Pools
* Server pools will allow a provider to 'schedule' a end users domain to one of many available DNS server pools

View File

@ -32,7 +32,5 @@ cfg.CONF(sys.argv[1:], project='moniker', prog='moniker-agent-bind9',
logging.setup('moniker') logging.setup('moniker')
serv = bind9_service.Service(cfg.CONF.host, cfg.CONF.agent_topic) launcher = service.launch(bind9_service.Service())
launcher = service.launch(serv)
launcher.wait() launcher.wait()

View File

@ -15,29 +15,22 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
import sys import sys
import eventlet
from moniker.openstack.common import cfg from moniker.openstack.common import cfg
from moniker.openstack.common import log as logging from moniker.openstack.common import log as logging
from moniker.api import app from moniker.openstack.common import service
from moniker.api import service as api_service
config_files = cfg.find_config_files(project='moniker', prog='moniker-api') eventlet.monkey_patch()
config_files = cfg.find_config_files(project='moniker',
prog='moniker-api')
config_files.append('./etc/moniker-api.conf') config_files.append('./etc/moniker-api.conf')
# Jerry Rig the keystone middleware.
from keystone.middleware import auth_token
auth_token.CONF = cfg.CONF
cfg.CONF.register_opts(auth_token.opts, group='keystone_authtoken')
cfg.CONF(sys.argv[1:], project='moniker', prog='moniker-api', cfg.CONF(sys.argv[1:], project='moniker', prog='moniker-api',
default_config_files=config_files) default_config_files=config_files)
logging.setup('moniker') logging.setup('moniker')
if cfg.CONF.verbose or cfg.CONF.debug: launcher = service.launch(api_service.Service())
app.debug = True launcher.wait()
if cfg.CONF.enable_keystone:
# Add Keystone Middleware
middleware_conf = {'delay_auth_decision': False}
app.wsgi_app = auth_token.AuthProtocol(app.wsgi_app, middleware_conf)
app.run(host=cfg.CONF.api_host, port=cfg.CONF.api_port)

View File

@ -32,7 +32,5 @@ cfg.CONF(sys.argv[1:], project='moniker', prog='moniker-central',
logging.setup('moniker') logging.setup('moniker')
serv = central_service.Service(cfg.CONF.host, cfg.CONF.central_topic) launcher = service.launch(central_service.Service())
launcher = service.launch(serv)
launcher.wait() launcher.wait()

View File

@ -0,0 +1,29 @@
[composite:osapi_dns]
use = egg:Paste#urlmap
/v1: osapi_dns_api_v1
[composite:osapi_dns_api_v1]
use = call:moniker.api.auth:pipeline_factory
noauth = noauth osapi_dns_app_v1
keystone = authtoken keystonecontext osapi_dns_app_v1
[app:osapi_dns_app_v1]
paste.app_factory = moniker.api.v1:factory
[filter:noauth]
paste.filter_factory = moniker.api.auth:NoAuthMiddleware.factory
[filter:keystonecontext]
paste.filter_factory = moniker.api.auth:KeystoneContextMiddleware.factory
[filter:authtoken]
paste.filter_factory = keystone.middleware.auth_token:filter_factory
service_protocol = http
service_host = 127.0.0.1
service_port = 5000
auth_host = 127.0.0.1
auth_port = 35357
auth_protocol = http
admin_tenant_name = %SERVICE_TENANT_NAME%
admin_user = %SERVICE_USER%
admin_password = %SERVICE_PASSWORD%

View File

@ -20,10 +20,5 @@ control_exchange = moniker
# #
allowed_rpc_exception_modules = moniker.exceptions, moniker.openstack.common.exception allowed_rpc_exception_modules = moniker.exceptions, moniker.openstack.common.exception
[keystone_authtoken] #
auth_host = 127.0.0.1 auth_strategy = noauth
auth_port = 35357
auth_protocol = http
admin_tenant_name = admin
admin_user = admin
admin_password = password

View File

@ -28,12 +28,4 @@ cfg.CONF.register_opts([
help='Templates Path'), help='Templates Path'),
cfg.StrOpt('templates-path', default='/usr/share/moniker/templates', cfg.StrOpt('templates-path', default='/usr/share/moniker/templates',
help='Templates Path'), help='Templates Path'),
# Temp Config Options
cfg.BoolOpt('enable-keystone', default=False,
help='Disable Keystone Integration'),
cfg.StrOpt('default-tenant', default='12345',
help='Tenant to use when keystone is disabled'),
cfg.StrOpt('default-user', default='12345',
help='User to use when keystone is disabled'),
]) ])

View File

@ -18,7 +18,6 @@ import subprocess
from jinja2 import Template from jinja2 import Template
from moniker.openstack.common import cfg from moniker.openstack.common import cfg
from moniker.openstack.common import log as logging from moniker.openstack.common import log as logging
from moniker.openstack.common import rpc
from moniker.openstack.common.rpc import service as rpc_service from moniker.openstack.common.rpc import service as rpc_service
from moniker.openstack.common.context import get_admin_context from moniker.openstack.common.context import get_admin_context
from moniker.central import api as central_api from moniker.central import api as central_api
@ -36,6 +35,11 @@ cfg.CONF.register_opts([
class Service(rpc_service.Service): class Service(rpc_service.Service):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
kwargs.update(
host=cfg.CONF.host,
topic=cfg.CONF.agent_topic
)
super(Service, self).__init__(*args, **kwargs) super(Service, self).__init__(*args, **kwargs)
# TODO: This is a hack to ensure the data dir is 100% up to date # TODO: This is a hack to ensure the data dir is 100% up to date

View File

@ -16,37 +16,20 @@
import flask import flask
from moniker.openstack.common import cfg from moniker.openstack.common import cfg
from moniker.openstack.common import jsonutils from moniker.openstack.common import jsonutils
from moniker.openstack.common.context import RequestContext
from moniker import central
from moniker.api import v1
from moniker.api import debug
# Allows us to serialize datetime's etc
flask.helpers.json = jsonutils
cfg.CONF.register_opts([ cfg.CONF.register_opts([
cfg.StrOpt('api_host', default='0.0.0.0', cfg.StrOpt('api_host', default='0.0.0.0',
help='API Host'), help='API Host'),
cfg.IntOpt('api_port', default=9001, cfg.IntOpt('api_port', default=9001,
help='API Port Number'), help='API Port Number'),
cfg.StrOpt('api_paste_config', default='moniker-api-paste.ini',
help='File name for the paste.deploy config for moniker-api'),
cfg.StrOpt('auth_strategy', default='noauth',
help='The strategy to use for auth. Supports noauth or '
'keystone'),
]) ])
app = flask.Flask('moniker.api')
# Blueprints # Allows us to serialize datetime's etc
app.register_blueprint(v1.blueprint, url_prefix='/v1') flask.helpers.json = jsonutils
app.register_blueprint(debug.blueprint, url_prefix='/debug')
@app.before_request
def attach_context():
request = flask.request
headers = request.headers
if cfg.CONF.enable_keystone:
request.context = RequestContext(auth_tok=headers.get('X-Auth-Token'),
user=headers.get('X-User-ID'),
tenant=headers.get('X-Tenant-ID'))
else:
request.context = RequestContext(user=cfg.CONF.default_user,
tenant=cfg.CONF.default_tenant)

51
moniker/api/auth.py Normal file
View File

@ -0,0 +1,51 @@
# Copyright 2012 Managed I.T.
#
# Author: Kiall Mac Innes <kiall@managedit.ie>
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from moniker.openstack.common.context import RequestContext
from moniker.openstack.common import cfg
from moniker.openstack.common import log as logging
from moniker import wsgi
LOG = logging.getLogger(__name__)
def pipeline_factory(loader, global_conf, **local_conf):
"""
A paste pipeline replica that keys off of auth_strategy.
Code nabbed from cinder.
"""
pipeline = local_conf[cfg.CONF.auth_strategy]
pipeline = pipeline.split()
filters = [loader.get_filter(n) for n in pipeline[:-1]]
app = loader.get_app(pipeline[-1])
filters.reverse()
for filter in filters:
app = filter(app)
return app
class KeystoneContextMiddleware(wsgi.Middleware):
def process_request(self, request):
headers = request.headers
context = RequestContext(auth_tok=headers.get('X-Auth-Token'),
user=headers.get('X-User-ID'),
tenant=headers.get('X-Tenant-ID'))
request.environ['context'] = context
class NoAuthMiddleware(wsgi.Middleware):
def process_request(self, request):
request.environ['context'] = RequestContext()

43
moniker/api/service.py Normal file
View File

@ -0,0 +1,43 @@
# Copyright 2012 Managed I.T.
#
# Author: Kiall Mac Innes <kiall@managedit.ie>
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from paste import deploy
from moniker.openstack.common import log as logging
from moniker.openstack.common import wsgi
from moniker.openstack.common import cfg
from moniker import utils
LOG = logging.getLogger(__name__)
class Service(wsgi.Service):
def __init__(self, backlog=128, threads=1000):
super(Service, self).__init__(threads)
self.host = cfg.CONF.api_host
self.port = cfg.CONF.api_port
self.backlog = backlog
config_path = cfg.CONF.api_paste_config
config_path = utils.find_config(config_path)
self.application = deploy.loadapp("config:%s" % config_path,
name='osapi_dns')
def start(self):
return super(Service, self).start(application=self.application,
port=self.port, host=self.host,
backlog=self.backlog)

View File

@ -20,3 +20,10 @@ blueprint = flask.Blueprint('v1', __name__)
import moniker.api.v1.servers import moniker.api.v1.servers
import moniker.api.v1.domains import moniker.api.v1.domains
import moniker.api.v1.records import moniker.api.v1.records
def factory(global_config, **local_conf):
app = flask.Flask('moniker.api.v1')
app.register_blueprint(blueprint)
return app

View File

@ -43,7 +43,7 @@ def get_domains_schema():
@blueprint.route('/domains', methods=['POST']) @blueprint.route('/domains', methods=['POST'])
def create_domain(): def create_domain():
context = flask.request.context context = flask.request.environ.get('context')
values = flask.request.json values = flask.request.json
try: try:
@ -67,7 +67,7 @@ def create_domain():
@blueprint.route('/domains', methods=['GET']) @blueprint.route('/domains', methods=['GET'])
def get_domains(): def get_domains():
context = flask.request.context context = flask.request.environ.get('context')
domains = central_api.get_domains(context) domains = central_api.get_domains(context)
@ -78,7 +78,7 @@ def get_domains():
@blueprint.route('/domains/<domain_id>', methods=['GET']) @blueprint.route('/domains/<domain_id>', methods=['GET'])
def get_domain(domain_id): def get_domain(domain_id):
context = flask.request.context context = flask.request.environ.get('context')
try: try:
domain = central_api.get_domain(context, domain_id) domain = central_api.get_domain(context, domain_id)
@ -94,7 +94,7 @@ def get_domain(domain_id):
@blueprint.route('/domains/<domain_id>', methods=['PUT']) @blueprint.route('/domains/<domain_id>', methods=['PUT'])
def update_domain(domain_id): def update_domain(domain_id):
context = flask.request.context context = flask.request.environ.get('context')
values = flask.request.json values = flask.request.json
try: try:
@ -116,7 +116,7 @@ def update_domain(domain_id):
@blueprint.route('/domains/<domain_id>', methods=['DELETE']) @blueprint.route('/domains/<domain_id>', methods=['DELETE'])
def delete_domain(domain_id): def delete_domain(domain_id):
context = flask.request.context context = flask.request.environ.get('context')
try: try:
central_api.delete_domain(context, domain_id) central_api.delete_domain(context, domain_id)

View File

@ -44,7 +44,7 @@ def get_records_schema():
@blueprint.route('/domains/<domain_id>/records', methods=['POST']) @blueprint.route('/domains/<domain_id>/records', methods=['POST'])
def create_record(domain_id): def create_record(domain_id):
context = flask.request.context context = flask.request.environ.get('context')
values = flask.request.json values = flask.request.json
try: try:
@ -69,7 +69,7 @@ def create_record(domain_id):
@blueprint.route('/domains/<domain_id>/records', methods=['GET']) @blueprint.route('/domains/<domain_id>/records', methods=['GET'])
def get_records(domain_id): def get_records(domain_id):
context = flask.request.context context = flask.request.environ.get('context')
records = central_api.get_records(context, domain_id) records = central_api.get_records(context, domain_id)
@ -78,7 +78,7 @@ def get_records(domain_id):
@blueprint.route('/domains/<domain_id>/records/<record_id>', methods=['GET']) @blueprint.route('/domains/<domain_id>/records/<record_id>', methods=['GET'])
def get_record(domain_id, record_id): def get_record(domain_id, record_id):
context = flask.request.context context = flask.request.environ.get('context')
try: try:
record = central_api.get_record(context, domain_id, record_id) record = central_api.get_record(context, domain_id, record_id)
@ -94,7 +94,7 @@ def get_record(domain_id, record_id):
@blueprint.route('/domains/<domain_id>/records/<record_id>', methods=['PUT']) @blueprint.route('/domains/<domain_id>/records/<record_id>', methods=['PUT'])
def update_record(domain_id, record_id): def update_record(domain_id, record_id):
context = flask.request.context context = flask.request.environ.get('context')
values = flask.request.json values = flask.request.json
try: try:
@ -118,7 +118,7 @@ def update_record(domain_id, record_id):
@blueprint.route('/domains/<domain_id>/records/<record_id>', @blueprint.route('/domains/<domain_id>/records/<record_id>',
methods=['DELETE']) methods=['DELETE'])
def delete_record(domain_id, record_id): def delete_record(domain_id, record_id):
context = flask.request.context context = flask.request.environ.get('context')
try: try:
central_api.delete_record(context, domain_id, record_id) central_api.delete_record(context, domain_id, record_id)

View File

@ -42,7 +42,7 @@ def get_servers_schema():
@blueprint.route('/servers', methods=['POST']) @blueprint.route('/servers', methods=['POST'])
def create_server(): def create_server():
context = flask.request.context context = flask.request.environ.get('context')
values = flask.request.json values = flask.request.json
try: try:
@ -66,7 +66,7 @@ def create_server():
@blueprint.route('/servers', methods=['GET']) @blueprint.route('/servers', methods=['GET'])
def get_servers(): def get_servers():
context = flask.request.context context = flask.request.environ.get('context')
servers = central_api.get_servers(context) servers = central_api.get_servers(context)
@ -77,7 +77,7 @@ def get_servers():
@blueprint.route('/servers/<server_id>', methods=['GET']) @blueprint.route('/servers/<server_id>', methods=['GET'])
def get_server(server_id): def get_server(server_id):
context = flask.request.context context = flask.request.environ.get('context')
try: try:
server = central_api.get_server(context, server_id) server = central_api.get_server(context, server_id)
@ -93,7 +93,7 @@ def get_server(server_id):
@blueprint.route('/servers/<server_id>', methods=['PUT']) @blueprint.route('/servers/<server_id>', methods=['PUT'])
def update_server(server_id): def update_server(server_id):
context = flask.request.context context = flask.request.environ.get('context')
values = flask.request.json values = flask.request.json
try: try:
@ -115,7 +115,7 @@ def update_server(server_id):
@blueprint.route('/servers/<server_id>', methods=['DELETE']) @blueprint.route('/servers/<server_id>', methods=['DELETE'])
def delete_server(server_id): def delete_server(server_id):
context = flask.request.context context = flask.request.environ.get('context')
try: try:
central_api.delete_server(context, server_id) central_api.delete_server(context, server_id)

View File

@ -15,9 +15,7 @@
# under the License. # under the License.
from moniker.openstack.common import cfg from moniker.openstack.common import cfg
from moniker.openstack.common import log as logging from moniker.openstack.common import log as logging
from moniker.openstack.common import rpc
from moniker.openstack.common.rpc import service as rpc_service from moniker.openstack.common.rpc import service as rpc_service
from moniker import exceptions
from moniker import database from moniker import database
from moniker import utils from moniker import utils
from moniker.agent import api as agent_api from moniker.agent import api as agent_api
@ -27,8 +25,12 @@ LOG = logging.getLogger(__name__)
class Service(rpc_service.Service): class Service(rpc_service.Service):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
kwargs.update(
host=cfg.CONF.host,
topic=cfg.CONF.central_topic
)
super(Service, self).__init__(*args, **kwargs) super(Service, self).__init__(*args, **kwargs)
self.init_database() self.init_database()

View File

@ -73,3 +73,9 @@ def get_driver(*args, **kwargs):
from moniker.database.sqlalchemy import Sqlalchemy from moniker.database.sqlalchemy import Sqlalchemy
return Sqlalchemy(*args, **kwargs) return Sqlalchemy(*args, **kwargs)
def reinitialize(*args, **kwargs):
""" Reset the DB to default - Used for testing purposes """
from moniker.database.sqlalchemy.session import reset_session
reset_session(*args, **kwargs)

View File

@ -14,7 +14,7 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
from sqlalchemy.exc import IntegrityError from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm.exc import NoResultFound, MultipleResultsFound from sqlalchemy.orm.exc import NoResultFound
from moniker.openstack.common import cfg from moniker.openstack.common import cfg
from moniker.openstack.common import log as logging from moniker.openstack.common import log as logging
from moniker import exceptions from moniker import exceptions
@ -22,6 +22,7 @@ from moniker.database import BaseDatabase
from moniker.database.sqlalchemy import models from moniker.database.sqlalchemy import models
from moniker.database.sqlalchemy.session import get_session from moniker.database.sqlalchemy.session import get_session
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
cfg.CONF.register_opts([ cfg.CONF.register_opts([
@ -33,7 +34,11 @@ cfg.CONF.register_opts([
class Sqlalchemy(BaseDatabase): class Sqlalchemy(BaseDatabase):
def __init__(self): def __init__(self):
self.session = get_session() self.session = get_session()
models.Base.metadata.create_all(self.session.bind) # HACK: Remove me self._initialize_database() # HACK: Remove me
def _initialize_database(self):
""" Semi-Private Method to create the database schema """
models.Base.metadata.create_all(self.session.bind)
# Server Methods # Server Methods
def create_server(self, context, values): def create_server(self, context, values):

View File

@ -14,8 +14,7 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
from uuid import uuid4 from uuid import uuid4
from sqlalchemy import (Column, DateTime, Boolean, String, Integer, ForeignKey, from sqlalchemy import Column, DateTime, String, Integer, ForeignKey, Enum
Enum)
from sqlalchemy.exc import IntegrityError from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import relationship, backref, object_mapper from sqlalchemy.orm import relationship, backref, object_mapper
from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.ext.declarative import declarative_base
@ -112,13 +111,13 @@ class Server(Base):
name = Column(String, nullable=False, unique=True) name = Column(String, nullable=False, unique=True)
ipv4 = Column(Inet, nullable=False, unique=True) ipv4 = Column(Inet, nullable=False, unique=True)
ipv6 = Column(Inet, default=None, unique=True) ipv6 = Column(Inet, default=None, nullable=True, unique=True)
class Domain(Base): class Domain(Base):
__tablename__ = 'domains' __tablename__ = 'domains'
tenant_id = Column(String, nullable=False) tenant_id = Column(String, default=None, nullable=True)
name = Column(String, nullable=False, unique=True) name = Column(String, nullable=False, unique=True)
email = Column(String, nullable=False) email = Column(String, nullable=False)

View File

@ -52,3 +52,10 @@ def get_engine():
engine.connect() engine.connect()
return engine return engine
def reset_session():
global _ENGINE, _SESSION
_ENGINE = None
_SESSION = None

View File

@ -19,6 +19,10 @@ class Base(Exception):
pass pass
class ConfigNotFound(Base):
pass
class InvalidObject(Base): class InvalidObject(Base):
pass pass

View File

@ -162,7 +162,9 @@ class Connection(object):
raise NotImplementedError() raise NotImplementedError()
def consume_in_thread_group(self, thread_group): def consume_in_thread_group(self, thread_group):
"""Spawn a thread to handle incoming messages in the supplied ThreadGroup. """
Spawn a thread to handle incoming messages in the supplied
ThreadGroup.
Spawn a thread that will be responsible for handling all incoming Spawn a thread that will be responsible for handling all incoming
messages for consumers that were set up on this connection. messages for consumers that were set up on this connection.

View File

@ -0,0 +1,728 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2011 OpenStack LLC.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""Utility methods for working with WSGI servers."""
import datetime
import eventlet
import eventlet.wsgi
eventlet.patcher.monkey_patch(all=False, socket=True)
import routes
import routes.middleware
import sys
import webob.dec
import webob.exc
from xml.dom import minidom
from xml.parsers import expat
from moniker.openstack.common import exception
from moniker.openstack.common.gettextutils import _
from moniker.openstack.common import log as logging
from moniker.openstack.common import jsonutils
from moniker.openstack.common import service
LOG = logging.getLogger(__name__)
def run_server(application, port):
"""Run a WSGI server with the given application."""
sock = eventlet.listen(('0.0.0.0', port))
eventlet.wsgi.server(sock, application)
class Service(service.Service):
"""
Provides a Service API for wsgi servers.
This gives us the ability to launch wsgi servers with the
Launcher classes in service.py.
"""
def __init__(self, threads=1000):
super(Service, self).__init__()
self.pool = eventlet.GreenPool(threads)
def start(self, application, port, host='0.0.0.0', backlog=128):
"""Start serving this service using the provided server instance.
:returns: None
"""
super(Service, self).start()
socket = eventlet.listen((host, port), backlog=backlog)
self.pool.spawn_n(self._run, application, socket)
def stop(self):
"""Stop serving this API.
:returns: None
"""
super(Service, self).stop()
def wait(self):
"""Wait until all servers have completed running."""
super(Service, self).wait()
try:
self.pool.waitall()
except KeyboardInterrupt:
pass
def _run(self, application, socket):
"""Start a WSGI server in a new green thread."""
logger = logging.getLogger('eventlet.wsgi.server')
eventlet.wsgi.server(socket, application, custom_pool=self.pool,
log=logging.WritableLogger(logger))
class Middleware(object):
"""
Base WSGI middleware wrapper. These classes require an application to be
initialized that will be called next. By default the middleware will
simply call its wrapped app, or you can override __call__ to customize its
behavior.
"""
def __init__(self, application):
self.application = application
def process_request(self, req):
"""
Called on each request.
If this returns None, the next application down the stack will be
executed. If it returns a response then that response will be returned
and execution will stop here.
"""
return None
def process_response(self, response):
"""Do whatever you'd like to the response."""
return response
@webob.dec.wsgify
def __call__(self, req):
response = self.process_request(req)
if response:
return response
response = req.get_response(self.application)
return self.process_response(response)
class Debug(Middleware):
"""
Helper class that can be inserted into any WSGI application chain
to get information about the request and response.
"""
@webob.dec.wsgify
def __call__(self, req):
print ("*" * 40) + " REQUEST ENVIRON"
for key, value in req.environ.items():
print key, "=", value
print
resp = req.get_response(self.application)
print ("*" * 40) + " RESPONSE HEADERS"
for (key, value) in resp.headers.iteritems():
print key, "=", value
print
resp.app_iter = self.print_generator(resp.app_iter)
return resp
@staticmethod
def print_generator(app_iter):
"""
Iterator that prints the contents of a wrapper string iterator
when iterated.
"""
print ("*" * 40) + " BODY"
for part in app_iter:
sys.stdout.write(part)
sys.stdout.flush()
yield part
print
class Router(object):
"""
WSGI middleware that maps incoming requests to WSGI apps.
"""
def __init__(self, mapper):
"""
Create a router for the given routes.Mapper.
Each route in `mapper` must specify a 'controller', which is a
WSGI app to call. You'll probably want to specify an 'action' as
well and have your controller be a wsgi.Controller, who will route
the request to the action method.
Examples:
mapper = routes.Mapper()
sc = ServerController()
# Explicit mapping of one route to a controller+action
mapper.connect(None, "/svrlist", controller=sc, action="list")
# Actions are all implicitly defined
mapper.resource("server", "servers", controller=sc)
# Pointing to an arbitrary WSGI app. You can specify the
# {path_info:.*} parameter so the target app can be handed just that
# section of the URL.
mapper.connect(None, "/v1.0/{path_info:.*}", controller=BlogApp())
"""
self.map = mapper
self._router = routes.middleware.RoutesMiddleware(self._dispatch,
self.map)
@webob.dec.wsgify
def __call__(self, req):
"""
Route the incoming request to a controller based on self.map.
If no match, return a 404.
"""
return self._router
@staticmethod
@webob.dec.wsgify
def _dispatch(req):
"""
Called by self._router after matching the incoming request to a route
and putting the information into req.environ. Either returns 404
or the routed WSGI app's response.
"""
match = req.environ['wsgiorg.routing_args'][1]
if not match:
return webob.exc.HTTPNotFound()
app = match['controller']
return app
class Request(webob.Request):
"""Add some Openstack API-specific logic to the base webob.Request."""
default_request_content_types = ('application/json', 'application/xml')
default_accept_types = ('application/json', 'application/xml')
default_accept_type = 'application/json'
def best_match_content_type(self, supported_content_types=None):
"""Determine the requested response content-type.
Based on the query extension then the Accept header.
Defaults to default_accept_type if we don't find a preference
"""
supported_content_types = (supported_content_types or
self.default_accept_types)
parts = self.path.rsplit('.', 1)
if len(parts) > 1:
ctype = 'application/{0}'.format(parts[1])
if ctype in supported_content_types:
return ctype
bm = self.accept.best_match(supported_content_types)
return bm or self.default_accept_type
def get_content_type(self, allowed_content_types=None):
"""Determine content type of the request body.
Does not do any body introspection, only checks header
"""
if not "Content-Type" in self.headers:
return None
content_type = self.content_type
allowed_content_types = (allowed_content_types or
self.default_request_content_types)
if content_type not in allowed_content_types:
raise exception.InvalidContentType(content_type=content_type)
return content_type
class Resource(object):
"""
WSGI app that handles (de)serialization and controller dispatch.
Reads routing information supplied by RoutesMiddleware and calls
the requested action method upon its deserializer, controller,
and serializer. Those three objects may implement any of the basic
controller action methods (create, update, show, index, delete)
along with any that may be specified in the api router. A 'default'
method may also be implemented to be used in place of any
non-implemented actions. Deserializer methods must accept a request
argument and return a dictionary. Controller methods must accept a
request argument. Additionally, they must also accept keyword
arguments that represent the keys returned by the Deserializer. They
may raise a webob.exc exception or return a dict, which will be
serialized by requested content type.
"""
def __init__(self, controller, deserializer=None, serializer=None):
"""
:param controller: object that implement methods created by routes lib
:param deserializer: object that supports webob request deserialization
through controller-like actions
:param serializer: object that supports webob response serialization
through controller-like actions
"""
self.controller = controller
self.serializer = serializer or ResponseSerializer()
self.deserializer = deserializer or RequestDeserializer()
@webob.dec.wsgify(RequestClass=Request)
def __call__(self, request):
"""WSGI method that controls (de)serialization and method dispatch."""
try:
action, action_args, accept = self.deserialize_request(request)
except exception.InvalidContentType:
msg = _("Unsupported Content-Type")
return webob.exc.HTTPUnsupportedMediaType(explanation=msg)
except exception.MalformedRequestBody:
msg = _("Malformed request body")
return webob.exc.HTTPBadRequest(explanation=msg)
action_result = self.execute_action(action, request, **action_args)
try:
return self.serialize_response(action, action_result, accept)
# return unserializable result (typically a webob exc)
except Exception:
return action_result
def deserialize_request(self, request):
return self.deserializer.deserialize(request)
def serialize_response(self, action, action_result, accept):
return self.serializer.serialize(action_result, accept, action)
def execute_action(self, action, request, **action_args):
return self.dispatch(self.controller, action, request, **action_args)
def dispatch(self, obj, action, *args, **kwargs):
"""Find action-specific method on self and call it."""
try:
method = getattr(obj, action)
except AttributeError:
method = getattr(obj, 'default')
return method(*args, **kwargs)
def get_action_args(self, request_environment):
"""Parse dictionary created by routes library."""
try:
args = request_environment['wsgiorg.routing_args'][1].copy()
except Exception:
return {}
try:
del args['controller']
except KeyError:
pass
try:
del args['format']
except KeyError:
pass
return args
class ActionDispatcher(object):
"""Maps method name to local methods through action name."""
def dispatch(self, *args, **kwargs):
"""Find and call local method."""
action = kwargs.pop('action', 'default')
action_method = getattr(self, str(action), self.default)
return action_method(*args, **kwargs)
def default(self, data):
raise NotImplementedError()
class DictSerializer(ActionDispatcher):
"""Default request body serialization"""
def serialize(self, data, action='default'):
return self.dispatch(data, action=action)
def default(self, data):
return ""
class JSONDictSerializer(DictSerializer):
"""Default JSON request body serialization"""
def default(self, data):
def sanitizer(obj):
if isinstance(obj, datetime.datetime):
_dtime = obj - datetime.timedelta(microseconds=obj.microsecond)
return _dtime.isoformat()
return obj
return jsonutils.dumps(data, default=sanitizer)
class XMLDictSerializer(DictSerializer):
def __init__(self, metadata=None, xmlns=None):
"""
:param metadata: information needed to deserialize xml into
a dictionary.
:param xmlns: XML namespace to include with serialized xml
"""
super(XMLDictSerializer, self).__init__()
self.metadata = metadata or {}
self.xmlns = xmlns
def default(self, data):
# We expect data to contain a single key which is the XML root.
root_key = data.keys()[0]
doc = minidom.Document()
node = self._to_xml_node(doc, self.metadata, root_key, data[root_key])
return self.to_xml_string(node)
def to_xml_string(self, node, has_atom=False):
self._add_xmlns(node, has_atom)
return node.toprettyxml(indent=' ', encoding='UTF-8')
#NOTE (ameade): the has_atom should be removed after all of the
# xml serializers and view builders have been updated to the current
# spec that required all responses include the xmlns:atom, the has_atom
# flag is to prevent current tests from breaking
def _add_xmlns(self, node, has_atom=False):
if self.xmlns is not None:
node.setAttribute('xmlns', self.xmlns)
if has_atom:
node.setAttribute('xmlns:atom', "http://www.w3.org/2005/Atom")
def _to_xml_node(self, doc, metadata, nodename, data):
"""Recursive method to convert data members to XML nodes."""
result = doc.createElement(nodename)
# Set the xml namespace if one is specified
# TODO(justinsb): We could also use prefixes on the keys
xmlns = metadata.get('xmlns', None)
if xmlns:
result.setAttribute('xmlns', xmlns)
#TODO(bcwaldon): accomplish this without a type-check
if type(data) is list:
collections = metadata.get('list_collections', {})
if nodename in collections:
metadata = collections[nodename]
for item in data:
node = doc.createElement(metadata['item_name'])
node.setAttribute(metadata['item_key'], str(item))
result.appendChild(node)
return result
singular = metadata.get('plurals', {}).get(nodename, None)
if singular is None:
if nodename.endswith('s'):
singular = nodename[:-1]
else:
singular = 'item'
for item in data:
node = self._to_xml_node(doc, metadata, singular, item)
result.appendChild(node)
#TODO(bcwaldon): accomplish this without a type-check
elif type(data) is dict:
collections = metadata.get('dict_collections', {})
if nodename in collections:
metadata = collections[nodename]
for k, v in data.items():
node = doc.createElement(metadata['item_name'])
node.setAttribute(metadata['item_key'], str(k))
text = doc.createTextNode(str(v))
node.appendChild(text)
result.appendChild(node)
return result
attrs = metadata.get('attributes', {}).get(nodename, {})
for k, v in data.items():
if k in attrs:
result.setAttribute(k, str(v))
else:
node = self._to_xml_node(doc, metadata, k, v)
result.appendChild(node)
else:
# Type is atom
node = doc.createTextNode(str(data))
result.appendChild(node)
return result
def _create_link_nodes(self, xml_doc, links):
link_nodes = []
for link in links:
link_node = xml_doc.createElement('atom:link')
link_node.setAttribute('rel', link['rel'])
link_node.setAttribute('href', link['href'])
if 'type' in link:
link_node.setAttribute('type', link['type'])
link_nodes.append(link_node)
return link_nodes
class ResponseHeadersSerializer(ActionDispatcher):
"""Default response headers serialization"""
def serialize(self, response, data, action):
self.dispatch(response, data, action=action)
def default(self, response, data):
response.status_int = 200
class ResponseSerializer(object):
"""Encode the necessary pieces into a response object"""
def __init__(self, body_serializers=None, headers_serializer=None):
self.body_serializers = {
'application/xml': XMLDictSerializer(),
'application/json': JSONDictSerializer(),
}
self.body_serializers.update(body_serializers or {})
self.headers_serializer = (headers_serializer or
ResponseHeadersSerializer())
def serialize(self, response_data, content_type, action='default'):
"""Serialize a dict into a string and wrap in a wsgi.Request object.
:param response_data: dict produced by the Controller
:param content_type: expected mimetype of serialized response body
"""
response = webob.Response()
self.serialize_headers(response, response_data, action)
self.serialize_body(response, response_data, content_type, action)
return response
def serialize_headers(self, response, data, action):
self.headers_serializer.serialize(response, data, action)
def serialize_body(self, response, data, content_type, action):
response.headers['Content-Type'] = content_type
if data is not None:
serializer = self.get_body_serializer(content_type)
response.body = serializer.serialize(data, action)
def get_body_serializer(self, content_type):
try:
return self.body_serializers[content_type]
except (KeyError, TypeError):
raise exception.InvalidContentType(content_type=content_type)
class RequestHeadersDeserializer(ActionDispatcher):
"""Default request headers deserializer"""
def deserialize(self, request, action):
return self.dispatch(request, action=action)
def default(self, request):
return {}
class RequestDeserializer(object):
"""Break up a Request object into more useful pieces."""
def __init__(self, body_deserializers=None, headers_deserializer=None,
supported_content_types=None):
self.supported_content_types = supported_content_types
self.body_deserializers = {
'application/xml': XMLDeserializer(),
'application/json': JSONDeserializer(),
}
self.body_deserializers.update(body_deserializers or {})
self.headers_deserializer = (headers_deserializer or
RequestHeadersDeserializer())
def deserialize(self, request):
"""Extract necessary pieces of the request.
:param request: Request object
:returns tuple of expected controller action name, dictionary of
keyword arguments to pass to the controller, the expected
content type of the response
"""
action_args = self.get_action_args(request.environ)
action = action_args.pop('action', None)
action_args.update(self.deserialize_headers(request, action))
action_args.update(self.deserialize_body(request, action))
accept = self.get_expected_content_type(request)
return (action, action_args, accept)
def deserialize_headers(self, request, action):
return self.headers_deserializer.deserialize(request, action)
def deserialize_body(self, request, action):
if not len(request.body) > 0:
LOG.debug(_("Empty body provided in request"))
return {}
try:
content_type = request.get_content_type()
except exception.InvalidContentType:
LOG.debug(_("Unrecognized Content-Type provided in request"))
raise
if content_type is None:
LOG.debug(_("No Content-Type provided in request"))
return {}
try:
deserializer = self.get_body_deserializer(content_type)
except exception.InvalidContentType:
LOG.debug(_("Unable to deserialize body as provided Content-Type"))
raise
return deserializer.deserialize(request.body, action)
def get_body_deserializer(self, content_type):
try:
return self.body_deserializers[content_type]
except (KeyError, TypeError):
raise exception.InvalidContentType(content_type=content_type)
def get_expected_content_type(self, request):
return request.best_match_content_type(self.supported_content_types)
def get_action_args(self, request_environment):
"""Parse dictionary created by routes library."""
try:
args = request_environment['wsgiorg.routing_args'][1].copy()
except Exception:
return {}
try:
del args['controller']
except KeyError:
pass
try:
del args['format']
except KeyError:
pass
return args
class TextDeserializer(ActionDispatcher):
"""Default request body deserialization"""
def deserialize(self, datastring, action='default'):
return self.dispatch(datastring, action=action)
def default(self, datastring):
return {}
class JSONDeserializer(TextDeserializer):
def _from_json(self, datastring):
try:
return jsonutils.loads(datastring)
except ValueError:
msg = _("cannot understand JSON")
raise exception.MalformedRequestBody(reason=msg)
def default(self, datastring):
return {'body': self._from_json(datastring)}
class XMLDeserializer(TextDeserializer):
def __init__(self, metadata=None):
"""
:param metadata: information needed to deserialize xml into
a dictionary.
"""
super(XMLDeserializer, self).__init__()
self.metadata = metadata or {}
def _from_xml(self, datastring):
plurals = set(self.metadata.get('plurals', {}))
try:
node = minidom.parseString(datastring).childNodes[0]
return {node.nodeName: self._from_xml_node(node, plurals)}
except expat.ExpatError:
msg = _("cannot understand XML")
raise exception.MalformedRequestBody(reason=msg)
def _from_xml_node(self, node, listnames):
"""Convert a minidom node to a simple Python type.
:param listnames: list of XML node names whose subnodes should
be considered list items.
"""
if len(node.childNodes) == 1 and node.childNodes[0].nodeType == 3:
return node.childNodes[0].nodeValue
elif node.nodeName in listnames:
return [self._from_xml_node(n, listnames) for n in node.childNodes]
else:
result = dict()
for attr in node.attributes.keys():
result[attr] = node.attributes[attr].nodeValue
for child in node.childNodes:
if child.nodeType != node.TEXT_NODE:
result[child.nodeName] = self._from_xml_node(child,
listnames)
return result
def find_first_child_named(self, parent, name):
"""Search a nodes children for the first child with a given name"""
for node in parent.childNodes:
if node.nodeName == name:
return node
return None
def find_children_named(self, parent, name):
"""Return all of a nodes children who have the given name"""
for node in parent.childNodes:
if node.nodeName == name:
yield node
def extract_text(self, node):
"""Get the text field contained by the given node"""
if len(node.childNodes) == 1:
child = node.childNodes[0]
if child.nodeType == child.TEXT_NODE:
return child.nodeValue
return ""
def default(self, datastring):
return {'body': self._from_xml(datastring)}

View File

@ -88,6 +88,9 @@ class CollectionSchema(object):
self.item_schema = item_schema self.item_schema = item_schema
def filter(self, obj): def filter(self, obj):
if not obj:
return []
return [self.item_schema.filter(o) for o in obj] return [self.item_schema.filter(o) for o in obj]
def raw(self): def raw(self):

View File

@ -15,13 +15,33 @@
# under the License. # under the License.
import unittest import unittest
import mox import mox
from moniker.openstack.common import cfg
from moniker.openstack.common.context import RequestContext, get_admin_context
from moniker.database import reinitialize as reinitialize_database
from moniker.database import sqlalchemy # Import for sql_connection cfg def.
class TestCase(unittest.TestCase): class TestCase(unittest.TestCase):
def setUp(self): def setUp(self):
super(TestCase, self).setUp() super(TestCase, self).setUp()
self.mox = mox.Mox() self.mox = mox.Mox()
self.config(database_driver='sqlalchemy', sql_connection='sqlite://',
rpc_backend='moniker.openstack.common.rpc.impl_fake',
notification_driver=[])
reinitialize_database()
def tearDown(self): def tearDown(self):
cfg.CONF.reset()
self.mox.UnsetStubs() self.mox.UnsetStubs()
super(TestCase, self).tearDown() super(TestCase, self).tearDown()
def config(self, **kwargs):
group = kwargs.pop('group', None)
for k, v in kwargs.iteritems():
cfg.CONF.set_override(k, v, group)
def get_context(self, **kwargs):
return RequestContext(**kwargs)
def get_admin_context(self):
return get_admin_context()

View File

@ -0,0 +1,23 @@
# Copyright 2012 Managed I.T.
#
# Author: Kiall Mac Innes <kiall@managedit.ie>
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from moniker.openstack.common import cfg
from moniker.central import service as central_service
from moniker.tests import TestCase
class CentralTestCase(TestCase):
def get_central_service(self):
return central_service.Service()

View File

@ -13,9 +13,337 @@
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
from moniker.tests import TestCase import random
from moniker.openstack.common import log as logging
from moniker.tests.central import CentralTestCase
from moniker import exceptions
LOG = logging.getLogger(__name__)
class ServiceTest(TestCase): class ServiceTest(CentralTestCase):
def test_something(self): def setUp(self):
assert(True) super(ServiceTest, self).setUp()
self.config(rpc_backend='moniker.openstack.common.rpc.impl_fake')
def test_init(self):
self.get_central_service()
def create_server(self, **kwargs):
context = kwargs.pop('context', self.get_admin_context())
service = kwargs.pop('service', self.get_central_service())
values = dict(
name='ns1.example.org',
ipv4='192.0.2.1',
ipv6='2001:db8::1',
)
values.update(kwargs)
return service.create_server(context, values=values)
def create_domain(self, **kwargs):
context = kwargs.pop('context', self.get_admin_context())
service = kwargs.pop('service', self.get_central_service())
values = dict(
name='example.com',
email='info@example.com',
)
values.update(kwargs)
return service.create_domain(context, values=values)
def create_record(self, domain_id, **kwargs):
context = kwargs.pop('context', self.get_admin_context())
service = kwargs.pop('service', self.get_central_service())
values = dict(
name='www.example.com',
type='A',
data='127.0.0.1'
)
values.update(kwargs)
return service.create_record(context, domain_id, values=values)
# Server Tests
def test_create_server(self):
context = self.get_admin_context()
service = self.get_central_service()
values = dict(
name='ns1.example.org',
ipv4='192.0.2.1',
ipv6='2001:db8::1',
)
# Create a server
server = service.create_server(context, values=values)
# Ensure all values have been set correctly
self.assertIsNotNone(server['id'])
self.assertEqual(server['name'], values['name'])
self.assertEqual(str(server['ipv4']), values['ipv4'])
self.assertEqual(str(server['ipv6']), values['ipv6'])
def test_get_servers(self):
context = self.get_admin_context()
service = self.get_central_service()
# Ensure we have no servers to start with.
servers = service.get_servers(context)
self.assertEqual(len(servers), 0)
# Create a single server (using default values)
self.create_server()
# Ensure we can retrieve the newly created server
servers = service.get_servers(context)
self.assertEqual(len(servers), 1)
self.assertEqual(servers[0]['name'], 'ns1.example.org')
# Create a second server
self.create_server(name='ns2.example.org', ipv4='192.0.2.2',
ipv6='2001:db8::2')
# Ensure we can retrieve both servers
servers = service.get_servers(context)
self.assertEqual(len(servers), 2)
self.assertEqual(servers[0]['name'], 'ns1.example.org')
self.assertEqual(servers[1]['name'], 'ns2.example.org')
def test_get_server(self):
context = self.get_admin_context()
service = self.get_central_service()
# Create a server
server_name = 'ns%d.example.org' % random.randint(10, 1000)
expected_server = self.create_server(name=server_name)
# Retrieve it, and ensure it's the same
server = service.get_server(context, expected_server['id'])
self.assertEqual(server['id'], expected_server['id'])
self.assertEqual(server['name'], expected_server['name'])
self.assertEqual(str(server['ipv4']), expected_server['ipv4'])
self.assertEqual(str(server['ipv6']), expected_server['ipv6'])
def test_update_server(self):
context = self.get_admin_context()
service = self.get_central_service()
# Create a server
expected_server = self.create_server()
# Update the server
values = dict(ipv4='127.0.0.1')
service.update_server(context, expected_server['id'], values=values)
# Fetch the server again
server = service.get_server(context, expected_server['id'])
# Ensure the server was updated correctly
self.assertEqual(str(server['ipv4']), '127.0.0.1')
def test_delete_server(self):
context = self.get_admin_context()
service = self.get_central_service()
# Create a server
server = self.create_server()
# Delete the server
service.delete_server(context, server['id'])
# Fetch the server again, ensuring an exception is raised
with self.assertRaises(exceptions.ServerNotFound):
service.get_server(context, server['id'])
# Domain Tests
def test_create_domain(self):
context = self.get_admin_context()
service = self.get_central_service()
values = dict(
name='example.com',
email='info@example.com'
)
# Create a domain
domain = service.create_domain(context, values=values)
# Ensure all values have been set correctly
self.assertIsNotNone(domain['id'])
self.assertEqual(domain['name'], values['name'])
self.assertEqual(domain['email'], values['email'])
def test_get_domains(self):
context = self.get_admin_context()
service = self.get_central_service()
# Ensure we have no domains to start with.
domains = service.get_domains(context)
self.assertEqual(len(domains), 0)
# Create a single domain (using default values)
self.create_domain()
# Ensure we can retrieve the newly created domain
domains = service.get_domains(context)
self.assertEqual(len(domains), 1)
self.assertEqual(domains[0]['name'], 'example.com')
# Create a second domain
self.create_domain(name='example.net')
# Ensure we can retrieve both domain
domains = service.get_domains(context)
self.assertEqual(len(domains), 2)
self.assertEqual(domains[0]['name'], 'example.com')
self.assertEqual(domains[1]['name'], 'example.net')
def test_get_domain(self):
context = self.get_admin_context()
service = self.get_central_service()
# Create a domain
domain_name = '%d.example.com' % random.randint(10, 1000)
expected_domain = self.create_domain(name=domain_name)
# Retrieve it, and ensure it's the same
domain = service.get_domain(context, expected_domain['id'])
self.assertEqual(domain['id'], expected_domain['id'])
self.assertEqual(domain['name'], expected_domain['name'])
self.assertEqual(domain['email'], expected_domain['email'])
def test_update_domain(self):
context = self.get_admin_context()
service = self.get_central_service()
# Create a domain
expected_domain = self.create_domain()
# Update the domain
values = dict(email='new@example.com')
service.update_domain(context, expected_domain['id'], values=values)
# Fetch the domain again
domain = service.get_domain(context, expected_domain['id'])
# Ensure the domain was updated correctly
self.assertEqual(domain['email'], 'new@example.com')
def test_delete_domain(self):
context = self.get_admin_context()
service = self.get_central_service()
# Create a domain
domain = self.create_domain()
# Delete the domain
service.delete_domain(context, domain['id'])
# Fetch the domain again, ensuring an exception is raised
with self.assertRaises(exceptions.DomainNotFound):
service.get_domain(context, domain['id'])
# Record Tests
def test_create_record(self):
context = self.get_admin_context()
service = self.get_central_service()
domain = self.create_domain()
values = dict(
name='www.example.com',
type='A',
data='127.0.0.1'
)
# Create a record
record = service.create_record(context, domain['id'], values=values)
# Ensure all values have been set correctly
self.assertIsNotNone(record['id'])
self.assertIsNotNone(record['ttl'])
self.assertEqual(record['name'], values['name'])
self.assertEqual(record['type'], values['type'])
self.assertEqual(record['data'], values['data'])
def test_get_records(self):
context = self.get_admin_context()
service = self.get_central_service()
domain = self.create_domain()
# Ensure we have no records to start with.
records = service.get_records(context, domain['id'])
self.assertEqual(len(records), 0)
# Create a single record (using default values)
self.create_record(domain['id'])
# Ensure we can retrieve the newly created record
records = service.get_records(context, domain['id'])
self.assertEqual(len(records), 1)
self.assertEqual(records[0]['name'], 'www.example.com')
# Create a second record
self.create_record(domain['id'], name='mail.example.com')
# Ensure we can retrieve both records
records = service.get_records(context, domain['id'])
self.assertEqual(len(records), 2)
self.assertEqual(records[0]['name'], 'www.example.com')
self.assertEqual(records[1]['name'], 'mail.example.com')
def test_get_record(self):
context = self.get_admin_context()
service = self.get_central_service()
domain = self.create_domain()
# Create a record
record_name = '%d.example.com' % random.randint(10, 1000)
expected_record = self.create_record(domain['id'], name=record_name)
# Retrieve it, and ensure it's the same
record = service.get_record(context, domain['id'],
expected_record['id'])
self.assertEqual(record['id'], expected_record['id'])
self.assertEqual(record['name'], expected_record['name'])
def test_update_record(self):
context = self.get_admin_context()
service = self.get_central_service()
domain = self.create_domain()
# Create a record
expected_record = self.create_record(domain['id'])
# Update the server
values = dict(data='127.0.0.2')
service.update_record(context, domain['id'], expected_record['id'],
values=values)
# Fetch the record again
record = service.get_record(context, domain['id'],
expected_record['id'])
# Ensure the record was updated correctly
self.assertEqual(record['data'], '127.0.0.2')
def test_delete_record(self):
context = self.get_admin_context()
service = self.get_central_service()
domain = self.create_domain()
# Create a record
record = self.create_record(domain['id'])
# Delete the record
service.delete_record(context, domain['id'], record['id'])
# Fetch the record again, ensuring an exception is raised
with self.assertRaises(exceptions.RecordNotFound):
service.get_record(context, domain['id'], record['id'])

View File

@ -13,10 +13,10 @@
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
import functools import os
from moniker.openstack.common import cfg from moniker.openstack.common import cfg
from moniker.openstack.common import rpc
from moniker.openstack.common.notifier import api as notifier_api from moniker.openstack.common.notifier import api as notifier_api
from moniker import exceptions
def notify(context, service, event_type, payload): def notify(context, service, event_type, payload):
@ -26,10 +26,28 @@ def notify(context, service, event_type, payload):
notifier_api.notify(context, publisher_id, event_type, priority, payload) notifier_api.notify(context, publisher_id, event_type, priority, payload)
def fanout_cast(context, topic, method, **kwargs): def find_config(config_path):
msg = { """ Find a configuration file using the given hint.
'method': method,
'args': kwargs
}
rpc.fanout_cast(context, topic, msg) Code nabbed from cinder.
:param config_path: Full or relative path to the config.
:returns: Full path of the config, if it exists.
:raises: `moniker.exceptions.ConfigNotFound`
"""
possible_locations = [
config_path,
os.path.join("etc", "moniker", config_path),
os.path.join("etc", config_path),
os.path.join(cfg.CONF.state_path, "etc", "moniker", config_path),
os.path.join(cfg.CONF.state_path, "etc", config_path),
os.path.join(cfg.CONF.state_path, config_path),
"/etc/moniker/%s" % config_path,
]
for path in possible_locations:
if os.path.exists(path):
return os.path.abspath(path)
raise exceptions.ConfigNotFound(os.path.abspath(config_path))

View File

@ -13,20 +13,15 @@
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
import flask from moniker.openstack.common import wsgi
from moniker.openstack.common import cfg
from moniker.openstack.common import log as logging
LOG = logging.getLogger(__name__)
blueprint = flask.Blueprint('debug', __name__)
@blueprint.route('/config', methods=['GET']) class Middleware(wsgi.Middleware):
def list_config(): @classmethod
return flask.jsonify(cfg.CONF) def factory(cls, global_config, **local_conf):
""" Used for paste app factories in paste.deploy config files """
def _factory(app):
return cls(app, **local_conf)
@blueprint.route('/context', methods=['GET']) return _factory
def list_config():
return flask.jsonify(flask.request.context.to_dict())

View File

@ -1,3 +1,3 @@
[DEFAULT] [DEFAULT]
modules=cfg,iniparser,rpc,importutils,excutils,local,jsonutils,gettextutils,timeutils,notifier,context,log,service,eventlet_backdoor,network_utils,threadgroup,loopingcall,utils,exception,setup modules=cfg,iniparser,rpc,importutils,excutils,local,jsonutils,gettextutils,timeutils,notifier,context,log,service,eventlet_backdoor,network_utils,threadgroup,loopingcall,utils,exception,setup,wsgi
base=moniker base=moniker

View File

@ -4,6 +4,10 @@ eventlet
sqlalchemy>=0.7 sqlalchemy>=0.7
jsonschema>=0.6 jsonschema>=0.6
ipaddr ipaddr
setuptools-git>=0.4
# Needed for Keystone Middleware # Needed for Keystone Middleware
https://launchpad.net/keystone/folsom/2012.2/+download/keystone-2012.2.tar.gz#egg=keystone https://launchpad.net/keystone/folsom/2012.2/+download/keystone-2012.2.tar.gz#egg=keystone
# Optional Stuff that is used by default
kombu

View File

@ -2,7 +2,3 @@ nose
mox mox
coverage coverage
pep8>=1.0 pep8>=1.0
setuptools-git>=0.4
# Optional Stuff used by default
kombu