Merge branch 'master' of github.com:managedit/moniker
This commit is contained in:
commit
bd58c9805b
2
.gitignore
vendored
2
.gitignore
vendored
@ -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
|
||||||
|
44
README.md
44
README.md
@ -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
|
||||||
|
@ -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()
|
||||||
|
@ -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)
|
|
||||||
|
@ -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()
|
||||||
|
29
etc/moniker-api-paste.ini.sample
Normal file
29
etc/moniker-api-paste.ini.sample
Normal 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%
|
@ -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
|
|
||||||
|
@ -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'),
|
|
||||||
])
|
])
|
||||||
|
@ -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
|
||||||
|
@ -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
51
moniker/api/auth.py
Normal 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
43
moniker/api/service.py
Normal 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)
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
@ -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)
|
||||||
|
@ -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)
|
||||||
|
@ -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()
|
||||||
|
@ -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)
|
||||||
|
@ -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):
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -19,6 +19,10 @@ class Base(Exception):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigNotFound(Base):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class InvalidObject(Base):
|
class InvalidObject(Base):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@ -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.
|
||||||
|
728
moniker/openstack/common/wsgi.py
Normal file
728
moniker/openstack/common/wsgi.py
Normal 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)}
|
@ -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):
|
||||||
|
@ -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()
|
||||||
|
@ -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()
|
@ -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'])
|
||||||
|
@ -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))
|
||||||
|
@ -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())
|
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
|
||||||
|
Loading…
Reference in New Issue
Block a user