Merge "API: fixes and error handling"
This commit is contained in:
commit
0c506e7a2c
@ -21,6 +21,7 @@ import pwd
|
||||
import pecan
|
||||
import sys
|
||||
import os
|
||||
import wsme_overrides
|
||||
from libra.api import config as api_config
|
||||
from libra.api import model
|
||||
from libra.api import acl
|
||||
@ -28,6 +29,10 @@ from libra.common.options import Options, setup_logging
|
||||
from eventlet import wsgi
|
||||
|
||||
|
||||
# Gets rid of pep8 error
|
||||
assert wsme_overrides
|
||||
|
||||
|
||||
def get_pecan_config():
|
||||
# Set up the pecan configuration
|
||||
filename = api_config.__file__.replace('.pyc', '.py')
|
||||
|
@ -26,9 +26,9 @@ app = {
|
||||
}
|
||||
}
|
||||
|
||||
wsme = {
|
||||
'debug': True
|
||||
}
|
||||
#wsme = {
|
||||
# 'debug': True
|
||||
#}
|
||||
|
||||
#database = {
|
||||
# 'username': 'root',
|
||||
|
@ -13,6 +13,7 @@
|
||||
# under the License.
|
||||
|
||||
import logging
|
||||
import socket
|
||||
# pecan imports
|
||||
from pecan import expose, abort, response, request
|
||||
from pecan.rest import RestController
|
||||
@ -26,9 +27,11 @@ from logs import LogsController
|
||||
# models
|
||||
from libra.api.model.lbaas import LoadBalancer, Device, Node, session
|
||||
from libra.api.model.lbaas import loadbalancers_devices, Limits
|
||||
from libra.api.model.validators import LBPut, LBPost, LBResp, LBVipResp, LBNode
|
||||
from libra.api.model.validators import LBPut, LBPost, LBResp, LBVipResp
|
||||
from libra.api.model.validators import LBRespNode
|
||||
from libra.api.library.gearman_client import submit_job
|
||||
from libra.api.acl import get_limited_to_project
|
||||
from libra.api.library.exp import OverLimit
|
||||
|
||||
|
||||
class LoadBalancersController(RestController):
|
||||
@ -58,13 +61,20 @@ class LoadBalancersController(RestController):
|
||||
|
||||
# if we don't have an id then we want a list of them own by this tenent
|
||||
if not load_balancer_id:
|
||||
load_balancers = {'loadBalancers': session.query(
|
||||
lbs = session.query(
|
||||
LoadBalancer.name, LoadBalancer.id, LoadBalancer.protocol,
|
||||
LoadBalancer.port, LoadBalancer.algorithm,
|
||||
LoadBalancer.status, LoadBalancer.created,
|
||||
LoadBalancer.updated
|
||||
).filter(LoadBalancer.tenantid == tenant_id).
|
||||
filter(LoadBalancer.status != 'DELETED').all()}
|
||||
).filter(LoadBalancer.tenantid == tenant_id).\
|
||||
filter(LoadBalancer.status != 'DELETED').all()
|
||||
|
||||
load_balancers = {'loadBalancers': []}
|
||||
|
||||
for lb in lbs:
|
||||
lb = lb._asdict()
|
||||
lb['id'] = str(lb['id'])
|
||||
load_balancers['loadBalancers'].append(lb)
|
||||
else:
|
||||
load_balancers = session.query(
|
||||
LoadBalancer.name, LoadBalancer.id, LoadBalancer.protocol,
|
||||
@ -97,6 +107,8 @@ class LoadBalancersController(RestController):
|
||||
vip = item._asdict()
|
||||
vip['type'] = 'PUBLIC'
|
||||
vip['ipVersion'] = 'IPV4'
|
||||
vip['address'] = vip['floatingIpAddr']
|
||||
del(vip['floatingIpAddr'])
|
||||
load_balancers['virtualIps'].append(vip)
|
||||
|
||||
nodes = session.query(
|
||||
@ -106,6 +118,10 @@ class LoadBalancersController(RestController):
|
||||
filter(LoadBalancer.id == load_balancer_id).\
|
||||
all()
|
||||
|
||||
load_balancers['id'] = str(load_balancers['id'])
|
||||
if not load_balancers['statusDescription']:
|
||||
load_balancers['statusDescription'] = ''
|
||||
|
||||
load_balancers['nodes'] = []
|
||||
for item in nodes:
|
||||
node = item._asdict()
|
||||
@ -114,9 +130,11 @@ class LoadBalancersController(RestController):
|
||||
else:
|
||||
node['condition'] = 'DISABLED'
|
||||
del node['enabled']
|
||||
node['port'] = str(node['port'])
|
||||
node['id'] = str(node['id'])
|
||||
load_balancers['nodes'].append(node)
|
||||
|
||||
session.commit()
|
||||
session.rollback()
|
||||
response.status = 200
|
||||
return load_balancers
|
||||
|
||||
@ -140,27 +158,48 @@ class LoadBalancersController(RestController):
|
||||
Returns: dict
|
||||
"""
|
||||
tenant_id = get_limited_to_project(request.headers)
|
||||
if body.nodes == Unset:
|
||||
if body.nodes == Unset or not len(body.nodes):
|
||||
raise ClientSideError(
|
||||
'At least one backend node needs to be supplied'
|
||||
)
|
||||
for node in body.nodes:
|
||||
if node.address == Unset:
|
||||
raise ClientSideError(
|
||||
'A supplied node has no address'
|
||||
)
|
||||
if node.port == Unset:
|
||||
raise ClientSideError(
|
||||
'Node {0} is missing a port'.format(node.address)
|
||||
)
|
||||
try:
|
||||
socket.inet_aton(node.address)
|
||||
except socket.error:
|
||||
raise ClientSideError(
|
||||
'IP Address {0} not valid'.format(node.address)
|
||||
)
|
||||
|
||||
lblimit = session.query(Limits.value).\
|
||||
filter(Limits.name == 'maxLoadBalancers').scalar()
|
||||
nodelimit = session.query(Limits.value).\
|
||||
filter(Limits.name == 'maxNodesPerLoadBalancer').scalar()
|
||||
namelimit = session.query(Limits.value).\
|
||||
filter(Limits.name == 'maxLoadBalancerNameLength').scalar()
|
||||
count = session.query(LoadBalancer).\
|
||||
filter(LoadBalancer.tenantid == tenant_id).\
|
||||
filter(LoadBalancer.status != 'DELETED').count()
|
||||
|
||||
if len(body.name) > namelimit:
|
||||
raise ClientSideError(
|
||||
'Length of Load Balancer name too long'
|
||||
)
|
||||
# TODO: this should probably be a 413, not sure how to do that yet
|
||||
if count >= lblimit:
|
||||
raise ClientSideError(
|
||||
raise OverLimit(
|
||||
'Account has hit limit of {0} Load Balancers'.
|
||||
format(lblimit)
|
||||
)
|
||||
if len(body.nodes) > nodelimit:
|
||||
raise ClientSideError(
|
||||
raise OverLimit(
|
||||
'Too many backend nodes supplied (limit is {0}'.
|
||||
format(nodelimit)
|
||||
)
|
||||
@ -261,7 +300,7 @@ class LoadBalancersController(RestController):
|
||||
enabled = 1
|
||||
out_node = Node(
|
||||
lbid=lb.id, port=node.port, address=node.address,
|
||||
enabled=enabled, status='ONLINE', weight=0
|
||||
enabled=enabled, status='ONLINE', weight=1
|
||||
)
|
||||
session.add(out_node)
|
||||
|
||||
@ -273,23 +312,23 @@ class LoadBalancersController(RestController):
|
||||
|
||||
try:
|
||||
return_data = LBResp()
|
||||
return_data.id = lb.id
|
||||
return_data.id = str(lb.id)
|
||||
return_data.name = lb.name
|
||||
return_data.protocol = lb.protocol
|
||||
return_data.port = lb.port
|
||||
return_data.port = str(lb.port)
|
||||
return_data.algorithm = lb.algorithm
|
||||
return_data.status = lb.status
|
||||
return_data.created = lb.created
|
||||
return_data.updated = lb.updated
|
||||
vip_resp = LBVipResp(
|
||||
address=device.floatingIpAddr, id=device.id,
|
||||
address=device.floatingIpAddr, id=str(device.id),
|
||||
type='PUBLIC', ipVersion='IPV4'
|
||||
)
|
||||
return_data.virtualIps = [vip_resp]
|
||||
return_data.nodes = []
|
||||
for node in body.nodes:
|
||||
out_node = LBNode(
|
||||
port=node.port, address=node.address,
|
||||
out_node = LBRespNode(
|
||||
port=str(node.port), address=node.address,
|
||||
condition=node.condition
|
||||
)
|
||||
return_data.nodes.append(out_node)
|
||||
@ -326,6 +365,12 @@ class LoadBalancersController(RestController):
|
||||
raise ClientSideError('Load Balancer ID is not valid')
|
||||
|
||||
if body.name != Unset:
|
||||
namelimit = session.query(Limits.value).\
|
||||
filter(Limits.name == 'maxLoadBalancerNameLength').scalar()
|
||||
if len(body.name) > namelimit:
|
||||
raise ClientSideError(
|
||||
'Length of Load Balancer name too long'
|
||||
)
|
||||
lb.name = body.name
|
||||
|
||||
if body.algorithm != Unset:
|
||||
@ -341,7 +386,7 @@ class LoadBalancersController(RestController):
|
||||
submit_job(
|
||||
'UPDATE', device.name, device.id, lb.id
|
||||
)
|
||||
return
|
||||
return ''
|
||||
|
||||
@expose('json')
|
||||
def delete(self, load_balancer_id):
|
||||
@ -388,7 +433,7 @@ class LoadBalancersController(RestController):
|
||||
'DELETE', device.name, device.id, lb.id
|
||||
)
|
||||
response.status = 202
|
||||
return None
|
||||
return ''
|
||||
except:
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.exception('Error communicating with load balancer pool')
|
||||
|
@ -13,6 +13,7 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import socket
|
||||
from pecan import expose, response, request, abort
|
||||
from pecan.rest import RestController
|
||||
import wsmeext.pecan as wsme_pecan
|
||||
@ -24,6 +25,7 @@ from libra.api.acl import get_limited_to_project
|
||||
from libra.api.model.validators import LBNodeResp, LBNodePost, NodeResp
|
||||
from libra.api.model.validators import LBNodePut
|
||||
from libra.api.library.gearman_client import submit_job
|
||||
from libra.api.library.exp import OverLimit
|
||||
|
||||
|
||||
class NodesController(RestController):
|
||||
@ -112,9 +114,25 @@ class NodesController(RestController):
|
||||
if self.lbid is None:
|
||||
raise ClientSideError('Load Balancer ID has not been supplied')
|
||||
|
||||
if not len(body.nodes):
|
||||
if body.nodes == Unset or not len(body.nodes):
|
||||
raise ClientSideError('No nodes have been supplied')
|
||||
|
||||
for node in body.nodes:
|
||||
if node.address == Unset:
|
||||
raise ClientSideError(
|
||||
'A supplied node has no address'
|
||||
)
|
||||
if node.port == Unset:
|
||||
raise ClientSideError(
|
||||
'Node {0} is missing a port'.format(node.address)
|
||||
)
|
||||
try:
|
||||
socket.inet_aton(node.address)
|
||||
except socket.error:
|
||||
raise ClientSideError(
|
||||
'IP Address {0} not valid'.format(node.address)
|
||||
)
|
||||
|
||||
load_balancer = session.query(LoadBalancer).\
|
||||
filter(LoadBalancer.tenantid == tenant_id).\
|
||||
filter(LoadBalancer.id == self.lbid).\
|
||||
@ -130,8 +148,8 @@ class NodesController(RestController):
|
||||
nodecount = session.query(Node).\
|
||||
filter(Node.lbid == self.lbid).count()
|
||||
|
||||
if (nodecount + len(body.nodes)) >= nodelimit:
|
||||
raise ClientSideError(
|
||||
if (nodecount + len(body.nodes)) > nodelimit:
|
||||
raise OverLimit(
|
||||
'Command would exceed Load Balancer node limit'
|
||||
)
|
||||
return_data = LBNodeResp()
|
||||
@ -143,7 +161,7 @@ class NodesController(RestController):
|
||||
enabled = 1
|
||||
new_node = Node(
|
||||
lbid=self.lbid, port=node.port, address=node.address,
|
||||
enabled=enabled, status='ONLINE', weight=0
|
||||
enabled=enabled, status='ONLINE', weight=1
|
||||
)
|
||||
session.add(new_node)
|
||||
session.flush()
|
||||
@ -206,7 +224,7 @@ class NodesController(RestController):
|
||||
submit_job(
|
||||
'UPDATE', device.name, device.id, lb.id
|
||||
)
|
||||
return
|
||||
return ''
|
||||
|
||||
@expose('json')
|
||||
def delete(self, node_id):
|
||||
|
27
libra/api/library/exp.py
Normal file
27
libra/api/library/exp.py
Normal file
@ -0,0 +1,27 @@
|
||||
# Copyright 2013 Hewlett-Packard Development Company, L.P.
|
||||
#
|
||||
# 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.
|
||||
|
||||
import six
|
||||
from wsme.exc import ClientSideError
|
||||
from wsme.utils import _
|
||||
|
||||
|
||||
class OverLimit(ClientSideError):
|
||||
def __init__(self, msg=''):
|
||||
self.msg = msg
|
||||
super(OverLimit, self).__init__()
|
||||
|
||||
@property
|
||||
def faultstring(self):
|
||||
return _(six.u("OverLimit: %s")) % (self.msg)
|
@ -107,7 +107,8 @@ class GearmanClientThread(object):
|
||||
# Device should never be used again
|
||||
device = session.query(Device).\
|
||||
filter(Device.id == data).first()
|
||||
device.status = 'DELETED'
|
||||
#TODO: change this to 'DELETED' when pool mgm deletes
|
||||
device.status = 'OFFLINE'
|
||||
session.commit()
|
||||
|
||||
def _set_error(self, device_id, errmsg):
|
||||
|
@ -24,6 +24,12 @@ class LBNode(Base):
|
||||
condition = Enum(wtypes.text, 'ENABLED', 'DISABLED')
|
||||
|
||||
|
||||
class LBRespNode(Base):
|
||||
port = wtypes.text
|
||||
address = wtypes.text
|
||||
condition = wtypes.text
|
||||
|
||||
|
||||
class LBNodePut(Base):
|
||||
condition = Enum(wtypes.text, 'ENABLED', 'DISABLED')
|
||||
|
||||
@ -63,7 +69,7 @@ class LBPut(Base):
|
||||
|
||||
|
||||
class LBVipResp(Base):
|
||||
id = int
|
||||
id = wtypes.text
|
||||
address = wtypes.text
|
||||
type = wtypes.text
|
||||
ipVersion = wtypes.text
|
||||
@ -77,13 +83,13 @@ class LBLogsPost(Base):
|
||||
|
||||
|
||||
class LBResp(Base):
|
||||
id = int
|
||||
id = wtypes.text
|
||||
name = wtypes.text
|
||||
protocol = wtypes.text
|
||||
port = int
|
||||
port = wtypes.text
|
||||
algorithm = wtypes.text
|
||||
status = wtypes.text
|
||||
created = wtypes.text
|
||||
updated = wtypes.text
|
||||
virtualIps = wsattr(['LBVipResp'])
|
||||
nodes = wsattr(['LBNode'])
|
||||
nodes = wsattr(['LBRespNode'])
|
||||
|
118
libra/api/wsme_overrides.py
Normal file
118
libra/api/wsme_overrides.py
Normal file
@ -0,0 +1,118 @@
|
||||
# Copyright 2013 Hewlett-Packard Development Company, L.P.
|
||||
#
|
||||
# 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.
|
||||
|
||||
import logging
|
||||
import traceback
|
||||
import functools
|
||||
import inspect
|
||||
import sys
|
||||
|
||||
import wsme
|
||||
import wsme.rest.args
|
||||
import wsme.rest.json
|
||||
import wsme.rest.xml
|
||||
import wsmeext.pecan
|
||||
import pecan
|
||||
from libra.api.library.exp import OverLimit
|
||||
|
||||
|
||||
def format_exception(excinfo, debug=False):
|
||||
"""Extract informations that can be sent to the client."""
|
||||
error = excinfo[1]
|
||||
log = logging.getLogger(__name__)
|
||||
if isinstance(error, wsme.exc.ClientSideError):
|
||||
r = dict(faultcode="Client",
|
||||
faultstring=error.faultstring)
|
||||
log.warning("Client-side error: %s" % r['faultstring'])
|
||||
return r
|
||||
else:
|
||||
faultstring = str(error)
|
||||
debuginfo = "\n".join(traceback.format_exception(*excinfo))
|
||||
|
||||
log.error('Server-side error: "%s". Detail: \n%s' % (
|
||||
faultstring, debuginfo))
|
||||
|
||||
if isinstance(error, ValueError):
|
||||
r = dict(faultcode="Client", faultstring=faultstring)
|
||||
else:
|
||||
r = dict(faultcode="Server", faultstring=faultstring)
|
||||
if debug:
|
||||
r['debuginfo'] = debuginfo
|
||||
return r
|
||||
|
||||
wsme.api.format_exception = format_exception
|
||||
|
||||
|
||||
def wsexpose(*args, **kwargs):
|
||||
pecan_json_decorate = pecan.expose(
|
||||
template='wsmejson:',
|
||||
content_type='application/json',
|
||||
generic=False)
|
||||
pecan_xml_decorate = pecan.expose(
|
||||
template='wsmexml:',
|
||||
content_type='application/xml',
|
||||
generic=False
|
||||
)
|
||||
sig = wsme.signature(*args, **kwargs)
|
||||
|
||||
def decorate(f):
|
||||
sig(f)
|
||||
funcdef = wsme.api.FunctionDefinition.get(f)
|
||||
funcdef.resolve_types(wsme.types.registry)
|
||||
|
||||
@functools.wraps(f)
|
||||
def callfunction(self, *args, **kwargs):
|
||||
try:
|
||||
args, kwargs = wsme.rest.args.get_args(
|
||||
funcdef, args, kwargs, pecan.request.params, None,
|
||||
pecan.request.body, pecan.request.content_type
|
||||
)
|
||||
if funcdef.pass_request:
|
||||
kwargs[funcdef.pass_request] = pecan.request
|
||||
result = f(self, *args, **kwargs)
|
||||
|
||||
# NOTE: Support setting of status_code with default 201
|
||||
pecan.response.status = funcdef.status_code
|
||||
if isinstance(result, wsme.api.Response):
|
||||
pecan.response.status = result.status_code
|
||||
result = result.obj
|
||||
|
||||
except:
|
||||
data = wsme.api.format_exception(
|
||||
sys.exc_info(),
|
||||
pecan.conf.get('wsme', {}).get('debug', False)
|
||||
)
|
||||
e = sys.exc_info()[1]
|
||||
if isinstance(e, OverLimit):
|
||||
pecan.response.status = 413
|
||||
elif data['faultcode'] == 'Client':
|
||||
pecan.response.status = 400
|
||||
else:
|
||||
pecan.response.status = 500
|
||||
return data
|
||||
|
||||
return dict(
|
||||
datatype=funcdef.return_type,
|
||||
result=result
|
||||
)
|
||||
|
||||
pecan_xml_decorate(callfunction)
|
||||
pecan_json_decorate(callfunction)
|
||||
pecan.util._cfg(callfunction)['argspec'] = inspect.getargspec(f)
|
||||
callfunction._wsme_definition = funcdef
|
||||
return callfunction
|
||||
|
||||
return decorate
|
||||
|
||||
wsmeext.pecan.wsexpose = wsexpose
|
Loading…
x
Reference in New Issue
Block a user