Merge "Ensure JSON responses result from failure"

This commit is contained in:
Jenkins 2017-03-28 04:51:31 +00:00 committed by Gerrit Code Review
commit 0e4280ede3
26 changed files with 328 additions and 856 deletions

View File

@ -1,13 +1,13 @@
from datetime import date
import os import os
from paste import deploy from paste import deploy
from flask import Flask, json from flask import Flask
from oslo_config import cfg from oslo_config import cfg
from oslo_log import log as logging from oslo_log import log as logging
from craton.api import v1 from craton.api import v1
from craton.util import JSON_KWARGS
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
@ -49,27 +49,11 @@ def create_app(global_config, **local_config):
return setup_app() return setup_app()
class JSONEncoder(json.JSONEncoder):
def default(self, o):
if isinstance(o, date):
return o.isoformat()
return json.JSONEncoder.default(self, o)
RESTFUL_JSON = {
"indent": 2,
"sort_keys": True,
"cls": JSONEncoder,
"separators": (",", ": "),
}
def setup_app(config=None): def setup_app(config=None):
app = Flask(__name__, static_folder=None) app = Flask(__name__, static_folder=None)
app.config.update( app.config.update(
PROPAGATE_EXCEPTIONS=True, PROPAGATE_EXCEPTIONS=True,
RESTFUL_JSON=RESTFUL_JSON, RESTFUL_JSON=JSON_KWARGS,
) )
app.register_blueprint(v1.bp, url_prefix='/v1') app.register_blueprint(v1.bp, url_prefix='/v1')
return app return app

View File

@ -4,11 +4,9 @@ from oslo_context import context
from oslo_log import log from oslo_log import log
from oslo_utils import uuidutils from oslo_utils import uuidutils
import flask
import json
from craton.db import api as dbapi from craton.db import api as dbapi
from craton import exceptions from craton import exceptions
from craton.util import handle_all_exceptions_decorator
LOG = log.getLogger(__name__) LOG = log.getLogger(__name__)
@ -34,20 +32,13 @@ class ContextMiddleware(base.Middleware):
request.environ['context'] = ctxt request.environ['context'] = ctxt
return ctxt return ctxt
def _invalid_project_id(self, project_id):
err_msg = json.dumps({
"message": "Project ID ('{}') is not a valid UUID".format(
project_id)
})
return flask.Response(response=err_msg, status=401,
headers={'Content-Type': 'application/json'})
class NoAuthContextMiddleware(ContextMiddleware): class NoAuthContextMiddleware(ContextMiddleware):
def __init__(self, application): def __init__(self, application):
self.application = application self.application = application
@handle_all_exceptions_decorator
def process_request(self, request): def process_request(self, request):
# Simply insert some dummy context info # Simply insert some dummy context info
self.make_context( self.make_context(
@ -72,11 +63,16 @@ class LocalAuthContextMiddleware(ContextMiddleware):
def __init__(self, application): def __init__(self, application):
self.application = application self.application = application
@handle_all_exceptions_decorator
def process_request(self, request): def process_request(self, request):
headers = request.headers headers = request.headers
project_id = headers.get('X-Auth-Project') project_id = headers.get('X-Auth-Project')
if not uuidutils.is_uuid_like(project_id): if not uuidutils.is_uuid_like(project_id):
return self._invalid_project_id(project_id) raise exceptions.AuthenticationError(
message="Project ID ('{}') is not a valid UUID".format(
project_id
)
)
ctx = self.make_context( ctx = self.make_context(
request, request,
@ -91,7 +87,7 @@ class LocalAuthContextMiddleware(ContextMiddleware):
user_info = dbapi.get_user_info(ctx, user_info = dbapi.get_user_info(ctx,
headers.get('X-Auth-User', None)) headers.get('X-Auth-User', None))
if user_info.api_key != headers.get('X-Auth-Token', None): if user_info.api_key != headers.get('X-Auth-Token', None):
return flask.Response(status=401) raise exceptions.AuthenticationError
if user_info.is_root: if user_info.is_root:
ctx.is_admin = True ctx.is_admin = True
ctx.is_admin_project = True ctx.is_admin_project = True
@ -102,10 +98,7 @@ class LocalAuthContextMiddleware(ContextMiddleware):
ctx.is_admin = False ctx.is_admin = False
ctx.is_admin_project = False ctx.is_admin_project = False
except exceptions.NotFound: except exceptions.NotFound:
return flask.Response(status=401) raise exceptions.AuthenticationError
except Exception as err:
LOG.error(err)
return flask.Response(status=500)
@classmethod @classmethod
def factory(cls, global_config, **local_config): def factory(cls, global_config, **local_config):
@ -116,11 +109,12 @@ class LocalAuthContextMiddleware(ContextMiddleware):
class KeystoneContextMiddleware(ContextMiddleware): class KeystoneContextMiddleware(ContextMiddleware):
@handle_all_exceptions_decorator
def process_request(self, request): def process_request(self, request):
headers = request.headers headers = request.headers
environ = request.environ environ = request.environ
if headers.get('X-Identity-Status', '').lower() != 'confirmed': if headers.get('X-Identity-Status', '').lower() != 'confirmed':
return flask.Response(status=401) raise exceptions.AuthenticationError
token_info = environ['keystone.token_info']['token'] token_info = environ['keystone.token_info']['token']
roles = (role['name'] for role in token_info['roles']) roles = (role['name'] for role in token_info['roles'])

View File

@ -2,10 +2,20 @@ from flask import Blueprint
import flask_restful as restful import flask_restful as restful
from craton.api.v1.routes import routes from craton.api.v1.routes import routes
from craton.util import handle_all_exceptions
class CratonApi(restful.Api):
def error_router(self, _, e):
return self.handle_error(e)
def handle_error(self, e):
return handle_all_exceptions(e)
bp = Blueprint('v1', __name__) bp = Blueprint('v1', __name__)
api = restful.Api(bp, catch_all_404s=True) api = CratonApi(bp, catch_all_404s=False)
for route in routes: for route in routes:
api.add_resource(route.pop('resource'), *route.pop('urls'), **route) api.add_resource(route.pop('resource'), *route.pop('urls'), **route)

View File

@ -1,19 +1,13 @@
import functools import functools
import inspect
import json
import re import re
import urllib.parse as urllib import urllib.parse as urllib
import decorator
import flask import flask
import flask_restful as restful import flask_restful as restful
from craton import api
from craton.api.v1.validators import ensure_project_exists from craton.api.v1.validators import ensure_project_exists
from craton.api.v1.validators import request_validate from craton.api.v1.validators import request_validate
from craton.api.v1.validators import response_filter from craton.api.v1.validators import response_filter
from craton import exceptions
SORT_KEY_SPLITTER = re.compile('[ ,]') SORT_KEY_SPLITTER = re.compile('[ ,]')
@ -23,30 +17,6 @@ class Resource(restful.Resource):
method_decorators = [request_validate, ensure_project_exists, method_decorators = [request_validate, ensure_project_exists,
response_filter] response_filter]
def error_response(self, status_code, message):
body = json.dumps(
{
'status': status_code,
'message': message
},
**api.RESTFUL_JSON,
)
resp = flask.make_response("{body}\n".format(body=body))
resp.status_code = status_code
return resp
@decorator.decorator
def http_codes(f, *args, **kwargs):
try:
return f(*args, **kwargs)
except exceptions.Base as err:
return args[0].error_response(err.code, err.message)
except Exception as err:
inspect.getmodule(f).LOG.error(
'Error during %s: %s' % (f.__qualname__, err))
return args[0].error_response(500, 'Unknown Error')
def pagination_context(function): def pagination_context(function):
@functools.wraps(function) @functools.wraps(function)

View File

@ -13,7 +13,6 @@ LOG = log.getLogger(__name__)
class Cells(base.Resource): class Cells(base.Resource):
@base.http_codes
@base.pagination_context @base.pagination_context
def get(self, context, request_args, pagination_params): def get(self, context, request_args, pagination_params):
"""Get all cells, with optional filtering.""" """Get all cells, with optional filtering."""
@ -30,7 +29,6 @@ class Cells(base.Resource):
response_body = {'cells': cells_obj, 'links': links} response_body = {'cells': cells_obj, 'links': links}
return jsonutils.to_primitive(response_body), 200, None return jsonutils.to_primitive(response_body), 200, None
@base.http_codes
def post(self, context, request_data): def post(self, context, request_data):
"""Create a new cell.""" """Create a new cell."""
json = util.copy_project_id_into_json(context, request_data) json = util.copy_project_id_into_json(context, request_data)
@ -51,19 +49,16 @@ class Cells(base.Resource):
class CellById(base.Resource): class CellById(base.Resource):
@base.http_codes
def get(self, context, id, request_args): def get(self, context, id, request_args):
cell_obj = dbapi.cells_get_by_id(context, id) cell_obj = dbapi.cells_get_by_id(context, id)
cell = utils.get_resource_with_vars(request_args, cell_obj) cell = utils.get_resource_with_vars(request_args, cell_obj)
return cell, 200, None return cell, 200, None
@base.http_codes
def put(self, context, id, request_data): def put(self, context, id, request_data):
"""Update existing cell.""" """Update existing cell."""
cell_obj = dbapi.cells_update(context, id, request_data) cell_obj = dbapi.cells_update(context, id, request_data)
return jsonutils.to_primitive(cell_obj), 200, None return jsonutils.to_primitive(cell_obj), 200, None
@base.http_codes
def delete(self, context, id): def delete(self, context, id):
"""Delete existing cell.""" """Delete existing cell."""
dbapi.cells_delete(context, id) dbapi.cells_delete(context, id)

View File

@ -13,7 +13,6 @@ LOG = log.getLogger(__name__)
class Clouds(base.Resource): class Clouds(base.Resource):
@base.http_codes
@base.pagination_context @base.pagination_context
def get(self, context, request_args, pagination_params): def get(self, context, request_args, pagination_params):
"""Get cloud(s) for the project. Get cloud details if """Get cloud(s) for the project. Get cloud details if
@ -46,7 +45,6 @@ class Clouds(base.Resource):
response_body = {'clouds': clouds_obj, 'links': links} response_body = {'clouds': clouds_obj, 'links': links}
return jsonutils.to_primitive(response_body), 200, None return jsonutils.to_primitive(response_body), 200, None
@base.http_codes
def post(self, context, request_data): def post(self, context, request_data):
"""Create a new cloud.""" """Create a new cloud."""
json = util.copy_project_id_into_json(context, request_data) json = util.copy_project_id_into_json(context, request_data)
@ -67,20 +65,17 @@ class Clouds(base.Resource):
class CloudsById(base.Resource): class CloudsById(base.Resource):
@base.http_codes
def get(self, context, id): def get(self, context, id):
cloud_obj = dbapi.clouds_get_by_id(context, id) cloud_obj = dbapi.clouds_get_by_id(context, id)
cloud = jsonutils.to_primitive(cloud_obj) cloud = jsonutils.to_primitive(cloud_obj)
cloud['variables'] = jsonutils.to_primitive(cloud_obj.variables) cloud['variables'] = jsonutils.to_primitive(cloud_obj.variables)
return cloud, 200, None return cloud, 200, None
@base.http_codes
def put(self, context, id, request_data): def put(self, context, id, request_data):
"""Update existing cloud.""" """Update existing cloud."""
cloud_obj = dbapi.clouds_update(context, id, request_data) cloud_obj = dbapi.clouds_update(context, id, request_data)
return jsonutils.to_primitive(cloud_obj), 200, None return jsonutils.to_primitive(cloud_obj), 200, None
@base.http_codes
def delete(self, context, id): def delete(self, context, id):
"""Delete existing cloud.""" """Delete existing cloud."""
dbapi.clouds_delete(context, id) dbapi.clouds_delete(context, id)

View File

@ -13,7 +13,6 @@ LOG = log.getLogger(__name__)
class Devices(base.Resource): class Devices(base.Resource):
@base.http_codes
@base.pagination_context @base.pagination_context
def get(self, context, request_args, pagination_params): def get(self, context, request_args, pagination_params):
"""Get all devices, with optional filtering.""" """Get all devices, with optional filtering."""

View File

@ -13,7 +13,6 @@ LOG = log.getLogger(__name__)
class Hosts(base.Resource): class Hosts(base.Resource):
@base.http_codes
@base.pagination_context @base.pagination_context
def get(self, context, request_args, pagination_params): def get(self, context, request_args, pagination_params):
"""Get all hosts for region, with optional filtering.""" """Get all hosts for region, with optional filtering."""
@ -35,7 +34,6 @@ class Hosts(base.Resource):
return response_body, 200, None return response_body, 200, None
@base.http_codes
def post(self, context, request_data): def post(self, context, request_data):
"""Create a new host.""" """Create a new host."""
json = util.copy_project_id_into_json(context, request_data) json = util.copy_project_id_into_json(context, request_data)
@ -58,7 +56,6 @@ class Hosts(base.Resource):
class HostById(base.Resource): class HostById(base.Resource):
@base.http_codes
def get(self, context, id, request_args): def get(self, context, id, request_args):
"""Get host by given id""" """Get host by given id"""
host_obj = dbapi.hosts_get_by_id(context, id) host_obj = dbapi.hosts_get_by_id(context, id)
@ -68,7 +65,6 @@ class HostById(base.Resource):
return host, 200, None return host, 200, None
@base.http_codes
def put(self, context, id, request_data): def put(self, context, id, request_data):
"""Update existing host data, or create if it does not exist.""" """Update existing host data, or create if it does not exist."""
host_obj = dbapi.hosts_update(context, id, request_data) host_obj = dbapi.hosts_update(context, id, request_data)
@ -79,7 +75,6 @@ class HostById(base.Resource):
return host, 200, None return host, 200, None
@base.http_codes
def delete(self, context, id): def delete(self, context, id):
"""Delete existing host.""" """Delete existing host."""
dbapi.hosts_delete(context, id) dbapi.hosts_delete(context, id)
@ -88,14 +83,12 @@ class HostById(base.Resource):
class HostsLabels(base.Resource): class HostsLabels(base.Resource):
@base.http_codes
def get(self, context, id): def get(self, context, id):
"""Get labels for given host device.""" """Get labels for given host device."""
host_obj = dbapi.hosts_get_by_id(context, id) host_obj = dbapi.hosts_get_by_id(context, id)
response = {"labels": list(host_obj.labels)} response = {"labels": list(host_obj.labels)}
return response, 200, None return response, 200, None
@base.http_codes
def put(self, context, id, request_data): def put(self, context, id, request_data):
""" """
Update existing device label entirely, or add if it does Update existing device label entirely, or add if it does
@ -105,7 +98,6 @@ class HostsLabels(base.Resource):
response = {"labels": list(resp.labels)} response = {"labels": list(resp.labels)}
return response, 200, None return response, 200, None
@base.http_codes
def delete(self, context, id, request_data): def delete(self, context, id, request_data):
"""Delete device label entirely.""" """Delete device label entirely."""
dbapi.hosts_labels_delete(context, id, request_data) dbapi.hosts_labels_delete(context, id, request_data)

View File

@ -14,7 +14,6 @@ LOG = log.getLogger(__name__)
class Networks(base.Resource): class Networks(base.Resource):
"""Controller for Networks resources.""" """Controller for Networks resources."""
@base.http_codes
@base.pagination_context @base.pagination_context
def get(self, context, request_args, pagination_params): def get(self, context, request_args, pagination_params):
"""Get all networks, with optional filtering.""" """Get all networks, with optional filtering."""
@ -30,7 +29,6 @@ class Networks(base.Resource):
response_body = {'networks': networks_obj, 'links': links} response_body = {'networks': networks_obj, 'links': links}
return jsonutils.to_primitive(response_body), 200, None return jsonutils.to_primitive(response_body), 200, None
@base.http_codes
def post(self, context, request_data): def post(self, context, request_data):
"""Create a new network.""" """Create a new network."""
json = util.copy_project_id_into_json(context, request_data) json = util.copy_project_id_into_json(context, request_data)
@ -53,7 +51,6 @@ class Networks(base.Resource):
class NetworkById(base.Resource): class NetworkById(base.Resource):
"""Controller for Networks by ID.""" """Controller for Networks by ID."""
@base.http_codes
def get(self, context, id): def get(self, context, id):
"""Get network by given id""" """Get network by given id"""
obj = dbapi.networks_get_by_id(context, id) obj = dbapi.networks_get_by_id(context, id)
@ -61,13 +58,11 @@ class NetworkById(base.Resource):
device['variables'] = jsonutils.to_primitive(obj.variables) device['variables'] = jsonutils.to_primitive(obj.variables)
return device, 200, None return device, 200, None
@base.http_codes
def put(self, context, id, request_data): def put(self, context, id, request_data):
"""Update existing network values.""" """Update existing network values."""
net_obj = dbapi.networks_update(context, id, request_data) net_obj = dbapi.networks_update(context, id, request_data)
return jsonutils.to_primitive(net_obj), 200, None return jsonutils.to_primitive(net_obj), 200, None
@base.http_codes
def delete(self, context, id): def delete(self, context, id):
"""Delete existing network.""" """Delete existing network."""
dbapi.networks_delete(context, id) dbapi.networks_delete(context, id)
@ -77,7 +72,6 @@ class NetworkById(base.Resource):
class NetworkDevices(base.Resource): class NetworkDevices(base.Resource):
"""Controller for Network Device resources.""" """Controller for Network Device resources."""
@base.http_codes
@base.pagination_context @base.pagination_context
def get(self, context, request_args, pagination_params): def get(self, context, request_args, pagination_params):
"""Get all network devices.""" """Get all network devices."""
@ -99,7 +93,6 @@ class NetworkDevices(base.Resource):
return response_body, 200, None return response_body, 200, None
@base.http_codes
def post(self, context, request_data): def post(self, context, request_data):
"""Create a new network device.""" """Create a new network device."""
json = util.copy_project_id_into_json(context, request_data) json = util.copy_project_id_into_json(context, request_data)
@ -123,7 +116,6 @@ class NetworkDevices(base.Resource):
class NetworkDeviceById(base.Resource): class NetworkDeviceById(base.Resource):
"""Controller for Network Devices by ID.""" """Controller for Network Devices by ID."""
@base.http_codes
def get(self, context, id, request_args): def get(self, context, id, request_args):
"""Get network device by given id""" """Get network device by given id"""
obj = dbapi.network_devices_get_by_id(context, id) obj = dbapi.network_devices_get_by_id(context, id)
@ -135,7 +127,6 @@ class NetworkDeviceById(base.Resource):
return device, 200, None return device, 200, None
@base.http_codes
def put(self, context, id, request_data): def put(self, context, id, request_data):
"""Update existing device values.""" """Update existing device values."""
net_obj = dbapi.network_devices_update(context, id, request_data) net_obj = dbapi.network_devices_update(context, id, request_data)
@ -145,7 +136,6 @@ class NetworkDeviceById(base.Resource):
return device, 200, None return device, 200, None
@base.http_codes
def delete(self, context, id): def delete(self, context, id):
"""Delete existing network device.""" """Delete existing network device."""
dbapi.network_devices_delete(context, id) dbapi.network_devices_delete(context, id)
@ -155,21 +145,18 @@ class NetworkDeviceById(base.Resource):
class NetworkDeviceLabels(base.Resource): class NetworkDeviceLabels(base.Resource):
"""Controller for Netowrk Device Labels.""" """Controller for Netowrk Device Labels."""
@base.http_codes
def get(self, context, id): def get(self, context, id):
"""Get labels for given network device.""" """Get labels for given network device."""
obj = dbapi.network_devices_get_by_id(context, id) obj = dbapi.network_devices_get_by_id(context, id)
response = {"labels": list(obj.labels)} response = {"labels": list(obj.labels)}
return response, 200, None return response, 200, None
@base.http_codes
def put(self, context, id, request_data): def put(self, context, id, request_data):
"""Update existing device label. Adds if it does not exist.""" """Update existing device label. Adds if it does not exist."""
resp = dbapi.network_devices_labels_update(context, id, request_data) resp = dbapi.network_devices_labels_update(context, id, request_data)
response = {"labels": list(resp.labels)} response = {"labels": list(resp.labels)}
return response, 200, None return response, 200, None
@base.http_codes
def delete(self, context, id, request_data): def delete(self, context, id, request_data):
"""Delete device label(s).""" """Delete device label(s)."""
dbapi.network_devices_labels_delete(context, id, request_data) dbapi.network_devices_labels_delete(context, id, request_data)
@ -179,7 +166,6 @@ class NetworkDeviceLabels(base.Resource):
class NetworkInterfaces(base.Resource): class NetworkInterfaces(base.Resource):
"""Controller for Netowrk Interfaces.""" """Controller for Netowrk Interfaces."""
@base.http_codes
@base.pagination_context @base.pagination_context
def get(self, context, request_args, pagination_params): def get(self, context, request_args, pagination_params):
"""Get all network interfaces.""" """Get all network interfaces."""
@ -190,7 +176,6 @@ class NetworkInterfaces(base.Resource):
response_body = {'network_interfaces': interfaces_obj, 'links': links} response_body = {'network_interfaces': interfaces_obj, 'links': links}
return jsonutils.to_primitive(response_body), 200, None return jsonutils.to_primitive(response_body), 200, None
@base.http_codes
def post(self, context, request_data): def post(self, context, request_data):
"""Create a new network interface.""" """Create a new network interface."""
json = util.copy_project_id_into_json(context, request_data) json = util.copy_project_id_into_json(context, request_data)
@ -207,7 +192,6 @@ class NetworkInterfaces(base.Resource):
class NetworkInterfaceById(base.Resource): class NetworkInterfaceById(base.Resource):
@base.http_codes
def get(self, context, id): def get(self, context, id):
"""Get network interface by given id""" """Get network interface by given id"""
obj = dbapi.network_interfaces_get_by_id(context, id) obj = dbapi.network_interfaces_get_by_id(context, id)
@ -215,13 +199,11 @@ class NetworkInterfaceById(base.Resource):
interface['variables'] = jsonutils.to_primitive(obj.variables) interface['variables'] = jsonutils.to_primitive(obj.variables)
return interface, 200, None return interface, 200, None
@base.http_codes
def put(self, context, id, request_data): def put(self, context, id, request_data):
"""Update existing network interface values.""" """Update existing network interface values."""
net_obj = dbapi.network_interfaces_update(context, id, request_data) net_obj = dbapi.network_interfaces_update(context, id, request_data)
return jsonutils.to_primitive(net_obj), 200, None return jsonutils.to_primitive(net_obj), 200, None
@base.http_codes
def delete(self, context, id): def delete(self, context, id):
"""Delete existing network interface.""" """Delete existing network interface."""
dbapi.network_interfaces_delete(context, id) dbapi.network_interfaces_delete(context, id)

View File

@ -13,7 +13,6 @@ LOG = log.getLogger(__name__)
class Regions(base.Resource): class Regions(base.Resource):
@base.http_codes
@base.pagination_context @base.pagination_context
def get(self, context, request_args, pagination_params): def get(self, context, request_args, pagination_params):
"""Get region(s) for the project. Get region details if """Get region(s) for the project. Get region details if
@ -46,7 +45,6 @@ class Regions(base.Resource):
response_body = {'regions': regions_obj, 'links': links} response_body = {'regions': regions_obj, 'links': links}
return jsonutils.to_primitive(response_body), 200, None return jsonutils.to_primitive(response_body), 200, None
@base.http_codes
def post(self, context, request_data): def post(self, context, request_data):
"""Create a new region.""" """Create a new region."""
json = util.copy_project_id_into_json(context, request_data) json = util.copy_project_id_into_json(context, request_data)
@ -67,19 +65,16 @@ class Regions(base.Resource):
class RegionsById(base.Resource): class RegionsById(base.Resource):
@base.http_codes
def get(self, context, id, request_args): def get(self, context, id, request_args):
region_obj = dbapi.regions_get_by_id(context, id) region_obj = dbapi.regions_get_by_id(context, id)
region = utils.get_resource_with_vars(request_args, region_obj) region = utils.get_resource_with_vars(request_args, region_obj)
return region, 200, None return region, 200, None
@base.http_codes
def put(self, context, id, request_data): def put(self, context, id, request_data):
"""Update existing region.""" """Update existing region."""
region_obj = dbapi.regions_update(context, id, request_data) region_obj = dbapi.regions_update(context, id, request_data)
return jsonutils.to_primitive(region_obj), 200, None return jsonutils.to_primitive(region_obj), 200, None
@base.http_codes
def delete(self, context, id): def delete(self, context, id):
"""Delete existing region.""" """Delete existing region."""
dbapi.regions_delete(context, id) dbapi.regions_delete(context, id)

View File

@ -12,7 +12,6 @@ LOG = log.getLogger(__name__)
class Projects(base.Resource): class Projects(base.Resource):
@base.http_codes
@base.pagination_context @base.pagination_context
def get(self, context, request_args, pagination_params): def get(self, context, request_args, pagination_params):
"""Get all projects. Requires super admin privileges.""" """Get all projects. Requires super admin privileges."""
@ -35,7 +34,6 @@ class Projects(base.Resource):
response_body = {'projects': projects_obj, 'links': links} response_body = {'projects': projects_obj, 'links': links}
return jsonutils.to_primitive(response_body), 200, None return jsonutils.to_primitive(response_body), 200, None
@base.http_codes
def post(self, context, request_data): def post(self, context, request_data):
"""Create a new project. Requires super admin privileges.""" """Create a new project. Requires super admin privileges."""
project_obj = dbapi.projects_create(context, request_data) project_obj = dbapi.projects_create(context, request_data)
@ -56,7 +54,6 @@ class Projects(base.Resource):
class ProjectById(base.Resource): class ProjectById(base.Resource):
@base.http_codes
def get(self, context, id): def get(self, context, id):
"""Get a project details by id. Requires super admin privileges.""" """Get a project details by id. Requires super admin privileges."""
project_obj = dbapi.projects_get_by_id(context, id) project_obj = dbapi.projects_get_by_id(context, id)
@ -64,7 +61,6 @@ class ProjectById(base.Resource):
project['variables'] = jsonutils.to_primitive(project_obj.variables) project['variables'] = jsonutils.to_primitive(project_obj.variables)
return project, 200, None return project, 200, None
@base.http_codes
def delete(self, context, id): def delete(self, context, id):
"""Delete existing project. Requires super admin privileges.""" """Delete existing project. Requires super admin privileges."""
dbapi.projects_delete(context, id) dbapi.projects_delete(context, id)

View File

@ -12,7 +12,6 @@ LOG = log.getLogger(__name__)
class Users(base.Resource): class Users(base.Resource):
@base.http_codes
@base.pagination_context @base.pagination_context
def get(self, context, request_args, pagination_params): def get(self, context, request_args, pagination_params):
"""Get all users. Requires project admin privileges.""" """Get all users. Requires project admin privileges."""
@ -37,7 +36,6 @@ class Users(base.Resource):
response_body = {'users': users_obj, 'links': links} response_body = {'users': users_obj, 'links': links}
return jsonutils.to_primitive(response_body), 200, None return jsonutils.to_primitive(response_body), 200, None
@base.http_codes
def post(self, context, request_data): def post(self, context, request_data):
"""Create a new user. Requires project admin privileges.""" """Create a new user. Requires project admin privileges."""
# NOTE(sulo): Instead of using context project_id from # NOTE(sulo): Instead of using context project_id from
@ -59,13 +57,11 @@ class Users(base.Resource):
class UserById(base.Resource): class UserById(base.Resource):
@base.http_codes
def get(self, context, id): def get(self, context, id):
"""Get a user details by id. Requires project admin privileges.""" """Get a user details by id. Requires project admin privileges."""
user_obj = dbapi.users_get_by_id(context, id) user_obj = dbapi.users_get_by_id(context, id)
return jsonutils.to_primitive(user_obj), 200, None return jsonutils.to_primitive(user_obj), 200, None
@base.http_codes
def delete(self, context, id): def delete(self, context, id):
"""Delete existing user. Requires project admin privileges.""" """Delete existing user. Requires project admin privileges."""
dbapi.users_delete(context, id) dbapi.users_delete(context, id)

View File

@ -14,7 +14,6 @@ LOG = log.getLogger(__name__)
class Variables(base.Resource): class Variables(base.Resource):
@base.http_codes
def get(self, context, resources, id, request_args=None): def get(self, context, resources, id, request_args=None):
"""Get variables for given resource.""" """Get variables for given resource."""
obj = dbapi.resource_get_by_id(context, resources, id) obj = dbapi.resource_get_by_id(context, resources, id)
@ -22,7 +21,6 @@ class Variables(base.Resource):
resp = {"variables": jsonutils.to_primitive(obj.vars)} resp = {"variables": jsonutils.to_primitive(obj.vars)}
return resp, 200, None return resp, 200, None
@base.http_codes
def put(self, context, resources, id, request_data): def put(self, context, resources, id, request_data):
""" """
Update existing resource variables, or create if it does Update existing resource variables, or create if it does
@ -34,7 +32,6 @@ class Variables(base.Resource):
resp = {"variables": jsonutils.to_primitive(obj.variables)} resp = {"variables": jsonutils.to_primitive(obj.variables)}
return resp, 200, None return resp, 200, None
@base.http_codes
def delete(self, context, resources, id, request_data): def delete(self, context, resources, id, request_data):
"""Delete resource variables.""" """Delete resource variables."""
# NOTE(sulo): this is not that great. Find a better way to do this. # NOTE(sulo): this is not that great. Find a better way to do this.

File diff suppressed because it is too large Load Diff

View File

@ -4,9 +4,7 @@
from functools import wraps from functools import wraps
from werkzeug.datastructures import MultiDict, Headers from werkzeug.datastructures import MultiDict, Headers
from flask import request, current_app from flask import request
from flask_restful import abort
from flask_restful.utils import unpack
from jsonschema import Draft4Validator from jsonschema import Draft4Validator
from oslo_log import log from oslo_log import log
@ -182,9 +180,12 @@ class FlaskValidatorAdaptor(object):
def validate(self, value): def validate(self, value):
value = self.type_convert(value) value = self.type_convert(value)
errors = list(e.message for e in self.validator.iter_errors(value)) errors = sorted(e.message for e in self.validator.iter_errors(value))
if errors: if errors:
abort(400, message='Bad Request', errors=errors) msg = "The request included the following errors:\n- {}".format(
"\n- ".join(errors)
)
raise exceptions.BadRequest(message=msg)
return merge_default(self.validator.schema, value) return merge_default(self.validator.schema, value)
@ -233,9 +234,6 @@ def response_filter(view):
def wrapper(*args, **kwargs): def wrapper(*args, **kwargs):
resp = view(*args, **kwargs) resp = view(*args, **kwargs)
if isinstance(resp, current_app.response_class):
return resp
endpoint = request.endpoint.partition('.')[-1] endpoint = request.endpoint.partition('.')[-1]
method = request.method method = request.method
if method == 'HEAD': if method == 'HEAD':
@ -248,12 +246,9 @@ def response_filter(view):
'filters.', 'filters.',
{"endpoint": endpoint, "method": method} {"endpoint": endpoint, "method": method}
) )
abort(500) raise exceptions.UnknownException
headers = None body, status, headers = resp
status = None
if isinstance(resp, tuple):
resp, status, headers = unpack(resp)
try: try:
schemas = resp_filter[status] schemas = resp_filter[status]
@ -263,17 +258,18 @@ def response_filter(view):
'filter "(%(endpoint)s, %(method)s)".', 'filter "(%(endpoint)s, %(method)s)".',
{"status": status, "endpoint": endpoint, "method": method} {"status": status, "endpoint": endpoint, "method": method}
) )
abort(500) raise exceptions.UnknownException
resp, errors = normalize(schemas['schema'], resp) body, errors = normalize(schemas['schema'], body)
if schemas['headers']: if schemas['headers']:
headers, header_errors = normalize( headers, header_errors = normalize(
{'properties': schemas['headers']}, headers) {'properties': schemas['headers']}, headers)
errors.extend(header_errors) errors.extend(header_errors)
if errors: if errors:
abort(500, message='Expectation Failed', errors=errors) LOG.error('Expectation Failed: %s', errors)
raise exceptions.UnknownException
return resp, status, headers return body, status, headers
return wrapper return wrapper

View File

@ -65,6 +65,11 @@ class DeviceNotFound(Base):
msg = "%(device_type)s device not found for ID %(id)s." msg = "%(device_type)s device not found for ID %(id)s."
class AuthenticationError(Base):
code = 401
msg = "The request could not be authenticated."
class AdminRequired(Base): class AdminRequired(Base):
code = 401 code = 401
msg = "This action requires the 'admin' role" msg = "This action requires the 'admin' role"

View File

@ -240,11 +240,19 @@ class TestCase(testtools.TestCase):
response.text response.text
) )
def assertFailureFormat(self, response):
if response.status_code >= 400:
body = response.json()
self.assertEqual(2, len(body))
self.assertEqual(response.status_code, body["status"])
self.assertIn("message", body)
def get(self, url, headers=None, **params): def get(self, url, headers=None, **params):
resp = self.session.get( resp = self.session.get(
url, verify=False, headers=headers, params=params, url, verify=False, headers=headers, params=params,
) )
self.assertJSON(resp) self.assertJSON(resp)
self.assertFailureFormat(resp)
return resp return resp
def post(self, url, headers=None, data=None): def post(self, url, headers=None, data=None):
@ -252,6 +260,7 @@ class TestCase(testtools.TestCase):
url, verify=False, headers=headers, json=data, url, verify=False, headers=headers, json=data,
) )
self.assertJSON(resp) self.assertJSON(resp)
self.assertFailureFormat(resp)
return resp return resp
def put(self, url, headers=None, data=None): def put(self, url, headers=None, data=None):
@ -259,6 +268,7 @@ class TestCase(testtools.TestCase):
url, verify=False, headers=headers, json=data, url, verify=False, headers=headers, json=data,
) )
self.assertJSON(resp) self.assertJSON(resp)
self.assertFailureFormat(resp)
return resp return resp
def delete(self, url, headers=None, body=None): def delete(self, url, headers=None, body=None):
@ -266,6 +276,7 @@ class TestCase(testtools.TestCase):
url, verify=False, headers=headers, json=body, url, verify=False, headers=headers, json=body,
) )
self.assertJSON(resp) self.assertJSON(resp)
self.assertFailureFormat(resp)
return resp return resp
def create_project(self, name, headers=None, variables=None): def create_project(self, name, headers=None, variables=None):

View File

@ -64,8 +64,11 @@ class APIV1CellTest(APIV1ResourceWithVariablesTestCase):
'cloud_id': self.cloud['id'], 'name': 'a', 'id': 3} 'cloud_id': self.cloud['id'], 'name': 'a', 'id': 3}
cell = self.post(url, data=payload) cell = self.post(url, data=payload)
self.assertEqual(400, cell.status_code) self.assertEqual(400, cell.status_code)
msg = ["Additional properties are not allowed ('id' was unexpected)"] msg = (
self.assertEqual(cell.json()['errors'], msg) "The request included the following errors:\n"
"- Additional properties are not allowed ('id' was unexpected)"
)
self.assertEqual(cell.json()['message'], msg)
def test_cell_create_with_extra_created_at_property_fails(self): def test_cell_create_with_extra_created_at_property_fails(self):
url = self.url + '/v1/cells' url = self.url + '/v1/cells'
@ -74,9 +77,12 @@ class APIV1CellTest(APIV1ResourceWithVariablesTestCase):
'created_at': "some date"} 'created_at': "some date"}
cell = self.post(url, data=payload) cell = self.post(url, data=payload)
self.assertEqual(400, cell.status_code) self.assertEqual(400, cell.status_code)
msg = ["Additional properties are not allowed " msg = (
"('created_at' was unexpected)"] "The request included the following errors:\n"
self.assertEqual(cell.json()['errors'], msg) "- Additional properties are not allowed "
"('created_at' was unexpected)"
)
self.assertEqual(cell.json()['message'], msg)
def test_cell_create_with_extra_updated_at_property_fails(self): def test_cell_create_with_extra_updated_at_property_fails(self):
url = self.url + '/v1/cells' url = self.url + '/v1/cells'
@ -85,9 +91,24 @@ class APIV1CellTest(APIV1ResourceWithVariablesTestCase):
'updated_at': "some date"} 'updated_at': "some date"}
cell = self.post(url, data=payload) cell = self.post(url, data=payload)
self.assertEqual(400, cell.status_code) self.assertEqual(400, cell.status_code)
msg = ["Additional properties are not allowed " msg = (
"('updated_at' was unexpected)"] "The request included the following errors:\n"
self.assertEqual(cell.json()['errors'], msg) "- Additional properties are not allowed "
"('updated_at' was unexpected)"
)
self.assertEqual(cell.json()['message'], msg)
def test_cell_create_missing_all_properties_fails(self):
url = self.url + '/v1/cells'
cell = self.post(url, data={})
self.assertEqual(400, cell.status_code)
msg = (
"The request included the following errors:\n"
"- 'cloud_id' is a required property\n"
"- 'name' is a required property\n"
"- 'region_id' is a required property"
)
self.assertEqual(cell.json()['message'], msg)
def test_cells_get_all_with_details(self): def test_cells_get_all_with_details(self):
self.create_cell('cell1', variables={'a': 'b'}) self.create_cell('cell1', variables={'a': 'b'})

View File

@ -40,8 +40,11 @@ class APIV1CloudTest(TestCase):
url = self.url + '/v1/clouds' url = self.url + '/v1/clouds'
resp = self.post(url, data=values) resp = self.post(url, data=values)
self.assertEqual(resp.status_code, 400) self.assertEqual(resp.status_code, 400)
err_msg = ["'name' is a required property"] err_msg = (
self.assertEqual(resp.json()['errors'], err_msg) "The request included the following errors:\n"
"- 'name' is a required property"
)
self.assertEqual(resp.json()['message'], err_msg)
def test_create_cloud_with_duplicate_name_fails(self): def test_create_cloud_with_duplicate_name_fails(self):
self.create_cloud("ORD135") self.create_cloud("ORD135")
@ -56,26 +59,45 @@ class APIV1CloudTest(TestCase):
url = self.url + '/v1/clouds' url = self.url + '/v1/clouds'
resp = self.post(url, data=values) resp = self.post(url, data=values)
self.assertEqual(resp.status_code, 400) self.assertEqual(resp.status_code, 400)
msg = ["Additional properties are not allowed ('id' was unexpected)"] msg = (
self.assertEqual(resp.json()['errors'], msg) "The request included the following errors:\n"
"- Additional properties are not allowed ('id' was unexpected)"
)
self.assertEqual(resp.json()['message'], msg)
def test_create_region_with_extra_created_at_property_fails(self): def test_create_region_with_extra_created_at_property_fails(self):
values = {"name": "test", "created_at": "some date"} values = {"name": "test", "created_at": "some date"}
url = self.url + '/v1/clouds' url = self.url + '/v1/clouds'
resp = self.post(url, data=values) resp = self.post(url, data=values)
self.assertEqual(resp.status_code, 400) self.assertEqual(resp.status_code, 400)
msg = ["Additional properties are not allowed " msg = (
"('created_at' was unexpected)"] "The request included the following errors:\n"
self.assertEqual(resp.json()['errors'], msg) "- Additional properties are not allowed "
"('created_at' was unexpected)"
)
self.assertEqual(resp.json()['message'], msg)
def test_create_region_with_extra_updated_at_property_fails(self): def test_create_region_with_extra_updated_at_property_fails(self):
values = {"name": "test", "updated_at": "some date"} values = {"name": "test", "updated_at": "some date"}
url = self.url + '/v1/clouds' url = self.url + '/v1/clouds'
resp = self.post(url, data=values) resp = self.post(url, data=values)
self.assertEqual(resp.status_code, 400) self.assertEqual(resp.status_code, 400)
msg = ["Additional properties are not allowed " msg = (
"('updated_at' was unexpected)"] "The request included the following errors:\n"
self.assertEqual(resp.json()['errors'], msg) "- Additional properties are not allowed "
"('updated_at' was unexpected)"
)
self.assertEqual(resp.json()['message'], msg)
def test_cloud_create_missing_all_properties_fails(self):
url = self.url + '/v1/clouds'
cloud = self.post(url, data={})
self.assertEqual(400, cloud.status_code)
msg = (
"The request included the following errors:\n"
"- 'name' is a required property"
)
self.assertEqual(cloud.json()['message'], msg)
def test_clouds_get_all(self): def test_clouds_get_all(self):
self.create_cloud("ORD1") self.create_cloud("ORD1")

View File

@ -69,8 +69,11 @@ class APIV1HostTest(DeviceTestBase, APIV1ResourceWithVariablesTestCase):
'cloud_id': self.cloud['id'], 'name': 'a', 'id': 1} 'cloud_id': self.cloud['id'], 'name': 'a', 'id': 1}
host = self.post(url, data=payload) host = self.post(url, data=payload)
self.assertEqual(400, host.status_code) self.assertEqual(400, host.status_code)
msg = ["Additional properties are not allowed ('id' was unexpected)"] msg = (
self.assertEqual(host.json()['errors'], msg) "The request included the following errors:\n"
"- Additional properties are not allowed ('id' was unexpected)"
)
self.assertEqual(host.json()['message'], msg)
def test_create_with_extra_created_at_property_fails(self): def test_create_with_extra_created_at_property_fails(self):
url = self.url + '/v1/hosts' url = self.url + '/v1/hosts'
@ -80,9 +83,12 @@ class APIV1HostTest(DeviceTestBase, APIV1ResourceWithVariablesTestCase):
'created_at': 'some date'} 'created_at': 'some date'}
host = self.post(url, data=payload) host = self.post(url, data=payload)
self.assertEqual(400, host.status_code) self.assertEqual(400, host.status_code)
msg = ["Additional properties are not allowed " msg = (
"('created_at' was unexpected)"] "The request included the following errors:\n"
self.assertEqual(host.json()['errors'], msg) "- Additional properties are not allowed "
"('created_at' was unexpected)"
)
self.assertEqual(host.json()['message'], msg)
def test_create_with_extra_updated_at_property_fails(self): def test_create_with_extra_updated_at_property_fails(self):
url = self.url + '/v1/hosts' url = self.url + '/v1/hosts'
@ -92,9 +98,26 @@ class APIV1HostTest(DeviceTestBase, APIV1ResourceWithVariablesTestCase):
'updated_at': 'some date'} 'updated_at': 'some date'}
host = self.post(url, data=payload) host = self.post(url, data=payload)
self.assertEqual(400, host.status_code) self.assertEqual(400, host.status_code)
msg = ["Additional properties are not allowed " msg = (
"('updated_at' was unexpected)"] "The request included the following errors:\n"
self.assertEqual(host.json()['errors'], msg) "- Additional properties are not allowed "
"('updated_at' was unexpected)"
)
self.assertEqual(host.json()['message'], msg)
def test_create_missing_all_properties_fails(self):
url = self.url + '/v1/hosts'
host = self.post(url, data={})
self.assertEqual(400, host.status_code)
msg = (
"The request included the following errors:\n"
"- 'cloud_id' is a required property\n"
"- 'device_type' is a required property\n"
"- 'ip_address' is a required property\n"
"- 'name' is a required property\n"
"- 'region_id' is a required property"
)
self.assertEqual(host.json()['message'], msg)
def test_create_with_parent_id(self): def test_create_with_parent_id(self):
parent = self.create_host( parent = self.create_host(

View File

@ -42,8 +42,11 @@ class APIV1NetworkSchemaTest(TestCase):
} }
network = self.post(self.networks_url, data=payload) network = self.post(self.networks_url, data=payload)
self.assertEqual(400, network.status_code) self.assertEqual(400, network.status_code)
msg = ["'region_id' is a required property"] msg = (
self.assertEqual(network.json()['errors'], msg) "The request included the following errors:\n"
"- 'region_id' is a required property"
)
self.assertEqual(network.json()['message'], msg)
def test_network_create_without_cloud_id_fails(self): def test_network_create_without_cloud_id_fails(self):
payload = { payload = {
@ -55,8 +58,11 @@ class APIV1NetworkSchemaTest(TestCase):
} }
network = self.post(self.networks_url, data=payload) network = self.post(self.networks_url, data=payload)
self.assertEqual(400, network.status_code) self.assertEqual(400, network.status_code)
msg = ["'cloud_id' is a required property"] msg = (
self.assertEqual(network.json()['errors'], msg) "The request included the following errors:\n"
"- 'cloud_id' is a required property"
)
self.assertEqual(network.json()['message'], msg)
def test_network_create_with_extra_id_property_fails(self): def test_network_create_with_extra_id_property_fails(self):
payload = { payload = {
@ -70,8 +76,11 @@ class APIV1NetworkSchemaTest(TestCase):
} }
network = self.post(self.networks_url, data=payload) network = self.post(self.networks_url, data=payload)
self.assertEqual(400, network.status_code) self.assertEqual(400, network.status_code)
msg = ["Additional properties are not allowed ('id' was unexpected)"] msg = (
self.assertEqual(network.json()['errors'], msg) "The request included the following errors:\n"
"- Additional properties are not allowed ('id' was unexpected)"
)
self.assertEqual(network.json()['message'], msg)
def test_network_create_with_extra_created_at_property_fails(self): def test_network_create_with_extra_created_at_property_fails(self):
payload = { payload = {
@ -85,9 +94,12 @@ class APIV1NetworkSchemaTest(TestCase):
} }
network = self.post(self.networks_url, data=payload) network = self.post(self.networks_url, data=payload)
self.assertEqual(400, network.status_code) self.assertEqual(400, network.status_code)
msg = ["Additional properties are not allowed ('created_at' was " msg = (
"unexpected)"] "The request included the following errors:\n"
self.assertEqual(network.json()['errors'], msg) "- Additional properties are not allowed ('created_at' was "
"unexpected)"
)
self.assertEqual(network.json()['message'], msg)
def test_network_create_with_extra_updated_at_property_fails(self): def test_network_create_with_extra_updated_at_property_fails(self):
payload = { payload = {
@ -101,9 +113,27 @@ class APIV1NetworkSchemaTest(TestCase):
} }
network = self.post(self.networks_url, data=payload) network = self.post(self.networks_url, data=payload)
self.assertEqual(400, network.status_code) self.assertEqual(400, network.status_code)
msg = ["Additional properties are not allowed ('updated_at' was " msg = (
"unexpected)"] "The request included the following errors:\n"
self.assertEqual(network.json()['errors'], msg) "- Additional properties are not allowed ('updated_at' was "
"unexpected)"
)
self.assertEqual(network.json()['message'], msg)
def test_network_create_missing_all_properties_fails(self):
url = self.url + '/v1/networks'
network = self.post(url, data={})
self.assertEqual(400, network.status_code)
msg = (
"The request included the following errors:\n"
"- 'cidr' is a required property\n"
"- 'cloud_id' is a required property\n"
"- 'gateway' is a required property\n"
"- 'name' is a required property\n"
"- 'netmask' is a required property\n"
"- 'region_id' is a required property"
)
self.assertEqual(network.json()['message'], msg)
def test_network_get_all_with_details(self): def test_network_get_all_with_details(self):
payload = { payload = {

View File

@ -99,3 +99,17 @@ class APIV1NetworkDeviceTest(DeviceTestBase):
url, data={'parent_id': grandchild['id']} url, data={'parent_id': grandchild['id']}
) )
self.assertEqual(400, parent_update_resp.status_code) self.assertEqual(400, parent_update_resp.status_code)
def test_network_device_create_missing_all_properties_fails(self):
url = self.url + '/v1/network-devices'
network_device = self.post(url, data={})
self.assertEqual(400, network_device.status_code)
msg = (
"The request included the following errors:\n"
"- 'cloud_id' is a required property\n"
"- 'device_type' is a required property\n"
"- 'ip_address' is a required property\n"
"- 'name' is a required property\n"
"- 'region_id' is a required property"
)
self.assertEqual(network_device.json()['message'], msg)

View File

@ -55,3 +55,16 @@ class APIv1NetworkInterfacesTest(functional.DeviceTestBase):
payload = {'port': 'asdf'} payload = {'port': 'asdf'}
response = self.put(url, data=payload) response = self.put(url, data=payload)
self.assertBadRequest(response) self.assertBadRequest(response)
def test_network_interface_create_missing_all_properties_fails(self):
url = self.url + '/v1/network-interfaces'
network_interface = self.post(url, data={})
self.assertEqual(400, network_interface.status_code)
msg = (
"The request included the following errors:\n"
"- 'device_id' is a required property\n"
"- 'interface_type' is a required property\n"
"- 'ip_address' is a required property\n"
"- 'name' is a required property"
)
self.assertEqual(network_interface.json()['message'], msg)

View File

@ -139,3 +139,13 @@ class APIV1ProjectTest(ProjectTests, APIV1ResourceWithVariablesTestCase):
variables=variables) variables=variables)
self.assert_vars_get_expected(project['id'], variables) self.assert_vars_get_expected(project['id'], variables)
self.assert_vars_can_be_deleted(project['id']) self.assert_vars_can_be_deleted(project['id'])
def test_project_create_missing_all_properties_fails(self):
url = self.url + '/v1/projects'
project = self.post(url, data={})
self.assertEqual(400, project.status_code)
msg = (
"The request included the following errors:\n"
"- 'name' is a required property"
)
self.assertEqual(project.json()['message'], msg)

View File

@ -63,16 +63,22 @@ class APIV1RegionTest(RegionTests):
url = self.url + '/v1/regions' url = self.url + '/v1/regions'
resp = self.post(url, data=values) resp = self.post(url, data=values)
self.assertEqual(resp.status_code, 400) self.assertEqual(resp.status_code, 400)
err_msg = ["'name' is a required property"] err_msg = (
self.assertEqual(resp.json()['errors'], err_msg) "The request included the following errors:\n"
"- 'name' is a required property"
)
self.assertEqual(resp.json()['message'], err_msg)
def test_create_region_with_no_cloud_id_fails(self): def test_create_region_with_no_cloud_id_fails(self):
values = {"name": "I don't work at all, you know."} values = {"name": "I don't work at all, you know."}
url = self.url + '/v1/regions' url = self.url + '/v1/regions'
resp = self.post(url, data=values) resp = self.post(url, data=values)
self.assertEqual(resp.status_code, 400) self.assertEqual(resp.status_code, 400)
err_msg = ["'cloud_id' is a required property"] err_msg = (
self.assertEqual(resp.json()['errors'], err_msg) "The request included the following errors:\n"
"- 'cloud_id' is a required property"
)
self.assertEqual(resp.json()['message'], err_msg)
def test_create_region_with_duplicate_name_fails(self): def test_create_region_with_duplicate_name_fails(self):
self.create_region("ORD135") self.create_region("ORD135")
@ -87,8 +93,11 @@ class APIV1RegionTest(RegionTests):
url = self.url + '/v1/regions' url = self.url + '/v1/regions'
resp = self.post(url, data=values) resp = self.post(url, data=values)
self.assertEqual(resp.status_code, 400) self.assertEqual(resp.status_code, 400)
msg = ["Additional properties are not allowed ('id' was unexpected)"] msg = (
self.assertEqual(resp.json()['errors'], msg) "The request included the following errors:\n"
"- Additional properties are not allowed ('id' was unexpected)"
)
self.assertEqual(resp.json()['message'], msg)
def test_create_region_with_extra_created_at_property_fails(self): def test_create_region_with_extra_created_at_property_fails(self):
values = {"name": "test", 'cloud_id': self.cloud['id'], values = {"name": "test", 'cloud_id': self.cloud['id'],
@ -96,9 +105,12 @@ class APIV1RegionTest(RegionTests):
url = self.url + '/v1/regions' url = self.url + '/v1/regions'
resp = self.post(url, data=values) resp = self.post(url, data=values)
self.assertEqual(resp.status_code, 400) self.assertEqual(resp.status_code, 400)
msg = ["Additional properties are not allowed " msg = (
"('created_at' was unexpected)"] "The request included the following errors:\n"
self.assertEqual(resp.json()['errors'], msg) "- Additional properties are not allowed "
"('created_at' was unexpected)"
)
self.assertEqual(resp.json()['message'], msg)
def test_create_region_with_extra_updated_at_property_fails(self): def test_create_region_with_extra_updated_at_property_fails(self):
values = {"name": "test", 'cloud_id': self.cloud['id'], values = {"name": "test", 'cloud_id': self.cloud['id'],
@ -106,9 +118,23 @@ class APIV1RegionTest(RegionTests):
url = self.url + '/v1/regions' url = self.url + '/v1/regions'
resp = self.post(url, data=values) resp = self.post(url, data=values)
self.assertEqual(resp.status_code, 400) self.assertEqual(resp.status_code, 400)
msg = ["Additional properties are not allowed " msg = (
"('updated_at' was unexpected)"] "The request included the following errors:\n"
self.assertEqual(resp.json()['errors'], msg) "- Additional properties are not allowed "
"('updated_at' was unexpected)"
)
self.assertEqual(resp.json()['message'], msg)
def test_region_create_missing_all_properties_fails(self):
url = self.url + '/v1/regions'
region = self.post(url, data={})
self.assertEqual(400, region.status_code)
msg = (
"The request included the following errors:\n"
"- 'cloud_id' is a required property\n"
"- 'name' is a required property"
)
self.assertEqual(region.json()['message'], msg)
def test_regions_get_all(self): def test_regions_get_all(self):
self.create_region("ORD1") self.create_region("ORD1")

View File

@ -1,4 +1,14 @@
"""Module containing generic utilies for Craton.""" """Module containing generic utilies for Craton."""
from datetime import date
from decorator import decorator
from flask import json, Response
import werkzeug.exceptions
from oslo_log import log
import craton.exceptions as exceptions
LOG = log.getLogger(__name__)
def copy_project_id_into_json(context, json, project_id_key='project_id'): def copy_project_id_into_json(context, json, project_id_key='project_id'):
@ -16,3 +26,57 @@ def copy_project_id_into_json(context, json, project_id_key='project_id'):
""" """
json[project_id_key] = getattr(context, 'tenant', '') json[project_id_key] = getattr(context, 'tenant', '')
return json return json
class JSONEncoder(json.JSONEncoder):
def default(self, o):
if isinstance(o, date):
return o.isoformat()
return json.JSONEncoder.default(self, o)
JSON_KWARGS = {
"indent": 2,
"sort_keys": True,
"cls": JSONEncoder,
"separators": (",", ": "),
}
def handle_all_exceptions(e):
"""Generate error Flask response object from exception."""
headers = [("Content-Type", "application/json")]
if isinstance(e, exceptions.Base):
message = e.message
status = e.code
elif isinstance(e, werkzeug.exceptions.HTTPException):
message = e.description
status = e.code
# Werkzeug exceptions can include additional headers, those should be
# kept unless the header is "Content-Type" which is set by this
# function.
headers.extend(
h for h in e.get_headers(None) if h[0].lower() != "content-type"
)
else:
LOG.exception(e)
e_ = exceptions.UnknownException
message = e_.message
status = e_.code
body = {
"message": message,
"status": status,
}
body_ = "{}\n".format(json.dumps(body, **JSON_KWARGS))
return Response(body_, status, headers)
@decorator
def handle_all_exceptions_decorator(fn, *args, **kwargs):
try:
return fn(*args, **kwargs)
except Exception as e:
return handle_all_exceptions(e)