Merge "API: fixes and error handling"

This commit is contained in:
Jenkins 2013-06-07 20:46:50 +00:00 committed by Gerrit Code Review
commit 0c506e7a2c
8 changed files with 249 additions and 29 deletions

View File

@ -21,6 +21,7 @@ import pwd
import pecan import pecan
import sys import sys
import os import os
import wsme_overrides
from libra.api import config as api_config from libra.api import config as api_config
from libra.api import model from libra.api import model
from libra.api import acl from libra.api import acl
@ -28,6 +29,10 @@ from libra.common.options import Options, setup_logging
from eventlet import wsgi from eventlet import wsgi
# Gets rid of pep8 error
assert wsme_overrides
def get_pecan_config(): def get_pecan_config():
# Set up the pecan configuration # Set up the pecan configuration
filename = api_config.__file__.replace('.pyc', '.py') filename = api_config.__file__.replace('.pyc', '.py')

View File

@ -26,9 +26,9 @@ app = {
} }
} }
wsme = { #wsme = {
'debug': True # 'debug': True
} #}
#database = { #database = {
# 'username': 'root', # 'username': 'root',

View File

@ -13,6 +13,7 @@
# under the License. # under the License.
import logging import logging
import socket
# pecan imports # pecan imports
from pecan import expose, abort, response, request from pecan import expose, abort, response, request
from pecan.rest import RestController from pecan.rest import RestController
@ -26,9 +27,11 @@ from logs import LogsController
# models # models
from libra.api.model.lbaas import LoadBalancer, Device, Node, session from libra.api.model.lbaas import LoadBalancer, Device, Node, session
from libra.api.model.lbaas import loadbalancers_devices, Limits 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.library.gearman_client import submit_job
from libra.api.acl import get_limited_to_project from libra.api.acl import get_limited_to_project
from libra.api.library.exp import OverLimit
class LoadBalancersController(RestController): 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 we don't have an id then we want a list of them own by this tenent
if not load_balancer_id: if not load_balancer_id:
load_balancers = {'loadBalancers': session.query( lbs = session.query(
LoadBalancer.name, LoadBalancer.id, LoadBalancer.protocol, LoadBalancer.name, LoadBalancer.id, LoadBalancer.protocol,
LoadBalancer.port, LoadBalancer.algorithm, LoadBalancer.port, LoadBalancer.algorithm,
LoadBalancer.status, LoadBalancer.created, LoadBalancer.status, LoadBalancer.created,
LoadBalancer.updated LoadBalancer.updated
).filter(LoadBalancer.tenantid == tenant_id). ).filter(LoadBalancer.tenantid == tenant_id).\
filter(LoadBalancer.status != 'DELETED').all()} 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: else:
load_balancers = session.query( load_balancers = session.query(
LoadBalancer.name, LoadBalancer.id, LoadBalancer.protocol, LoadBalancer.name, LoadBalancer.id, LoadBalancer.protocol,
@ -97,6 +107,8 @@ class LoadBalancersController(RestController):
vip = item._asdict() vip = item._asdict()
vip['type'] = 'PUBLIC' vip['type'] = 'PUBLIC'
vip['ipVersion'] = 'IPV4' vip['ipVersion'] = 'IPV4'
vip['address'] = vip['floatingIpAddr']
del(vip['floatingIpAddr'])
load_balancers['virtualIps'].append(vip) load_balancers['virtualIps'].append(vip)
nodes = session.query( nodes = session.query(
@ -106,6 +118,10 @@ class LoadBalancersController(RestController):
filter(LoadBalancer.id == load_balancer_id).\ filter(LoadBalancer.id == load_balancer_id).\
all() all()
load_balancers['id'] = str(load_balancers['id'])
if not load_balancers['statusDescription']:
load_balancers['statusDescription'] = ''
load_balancers['nodes'] = [] load_balancers['nodes'] = []
for item in nodes: for item in nodes:
node = item._asdict() node = item._asdict()
@ -114,9 +130,11 @@ class LoadBalancersController(RestController):
else: else:
node['condition'] = 'DISABLED' node['condition'] = 'DISABLED'
del node['enabled'] del node['enabled']
node['port'] = str(node['port'])
node['id'] = str(node['id'])
load_balancers['nodes'].append(node) load_balancers['nodes'].append(node)
session.commit() session.rollback()
response.status = 200 response.status = 200
return load_balancers return load_balancers
@ -140,27 +158,48 @@ class LoadBalancersController(RestController):
Returns: dict Returns: dict
""" """
tenant_id = get_limited_to_project(request.headers) tenant_id = get_limited_to_project(request.headers)
if body.nodes == Unset: if body.nodes == Unset or not len(body.nodes):
raise ClientSideError( raise ClientSideError(
'At least one backend node needs to be supplied' '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).\ lblimit = session.query(Limits.value).\
filter(Limits.name == 'maxLoadBalancers').scalar() filter(Limits.name == 'maxLoadBalancers').scalar()
nodelimit = session.query(Limits.value).\ nodelimit = session.query(Limits.value).\
filter(Limits.name == 'maxNodesPerLoadBalancer').scalar() filter(Limits.name == 'maxNodesPerLoadBalancer').scalar()
namelimit = session.query(Limits.value).\
filter(Limits.name == 'maxLoadBalancerNameLength').scalar()
count = session.query(LoadBalancer).\ count = session.query(LoadBalancer).\
filter(LoadBalancer.tenantid == tenant_id).\ filter(LoadBalancer.tenantid == tenant_id).\
filter(LoadBalancer.status != 'DELETED').count() 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 # TODO: this should probably be a 413, not sure how to do that yet
if count >= lblimit: if count >= lblimit:
raise ClientSideError( raise OverLimit(
'Account has hit limit of {0} Load Balancers'. 'Account has hit limit of {0} Load Balancers'.
format(lblimit) format(lblimit)
) )
if len(body.nodes) > nodelimit: if len(body.nodes) > nodelimit:
raise ClientSideError( raise OverLimit(
'Too many backend nodes supplied (limit is {0}'. 'Too many backend nodes supplied (limit is {0}'.
format(nodelimit) format(nodelimit)
) )
@ -261,7 +300,7 @@ class LoadBalancersController(RestController):
enabled = 1 enabled = 1
out_node = Node( out_node = Node(
lbid=lb.id, port=node.port, address=node.address, 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) session.add(out_node)
@ -273,23 +312,23 @@ class LoadBalancersController(RestController):
try: try:
return_data = LBResp() return_data = LBResp()
return_data.id = lb.id return_data.id = str(lb.id)
return_data.name = lb.name return_data.name = lb.name
return_data.protocol = lb.protocol return_data.protocol = lb.protocol
return_data.port = lb.port return_data.port = str(lb.port)
return_data.algorithm = lb.algorithm return_data.algorithm = lb.algorithm
return_data.status = lb.status return_data.status = lb.status
return_data.created = lb.created return_data.created = lb.created
return_data.updated = lb.updated return_data.updated = lb.updated
vip_resp = LBVipResp( vip_resp = LBVipResp(
address=device.floatingIpAddr, id=device.id, address=device.floatingIpAddr, id=str(device.id),
type='PUBLIC', ipVersion='IPV4' type='PUBLIC', ipVersion='IPV4'
) )
return_data.virtualIps = [vip_resp] return_data.virtualIps = [vip_resp]
return_data.nodes = [] return_data.nodes = []
for node in body.nodes: for node in body.nodes:
out_node = LBNode( out_node = LBRespNode(
port=node.port, address=node.address, port=str(node.port), address=node.address,
condition=node.condition condition=node.condition
) )
return_data.nodes.append(out_node) return_data.nodes.append(out_node)
@ -326,6 +365,12 @@ class LoadBalancersController(RestController):
raise ClientSideError('Load Balancer ID is not valid') raise ClientSideError('Load Balancer ID is not valid')
if body.name != Unset: 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 lb.name = body.name
if body.algorithm != Unset: if body.algorithm != Unset:
@ -341,7 +386,7 @@ class LoadBalancersController(RestController):
submit_job( submit_job(
'UPDATE', device.name, device.id, lb.id 'UPDATE', device.name, device.id, lb.id
) )
return return ''
@expose('json') @expose('json')
def delete(self, load_balancer_id): def delete(self, load_balancer_id):
@ -388,7 +433,7 @@ class LoadBalancersController(RestController):
'DELETE', device.name, device.id, lb.id 'DELETE', device.name, device.id, lb.id
) )
response.status = 202 response.status = 202
return None return ''
except: except:
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
logger.exception('Error communicating with load balancer pool') logger.exception('Error communicating with load balancer pool')

View File

@ -13,6 +13,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.
import socket
from pecan import expose, response, request, abort from pecan import expose, response, request, abort
from pecan.rest import RestController from pecan.rest import RestController
import wsmeext.pecan as wsme_pecan 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 LBNodeResp, LBNodePost, NodeResp
from libra.api.model.validators import LBNodePut from libra.api.model.validators import LBNodePut
from libra.api.library.gearman_client import submit_job from libra.api.library.gearman_client import submit_job
from libra.api.library.exp import OverLimit
class NodesController(RestController): class NodesController(RestController):
@ -112,9 +114,25 @@ class NodesController(RestController):
if self.lbid is None: if self.lbid is None:
raise ClientSideError('Load Balancer ID has not been supplied') 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') 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).\ load_balancer = session.query(LoadBalancer).\
filter(LoadBalancer.tenantid == tenant_id).\ filter(LoadBalancer.tenantid == tenant_id).\
filter(LoadBalancer.id == self.lbid).\ filter(LoadBalancer.id == self.lbid).\
@ -130,8 +148,8 @@ class NodesController(RestController):
nodecount = session.query(Node).\ nodecount = session.query(Node).\
filter(Node.lbid == self.lbid).count() filter(Node.lbid == self.lbid).count()
if (nodecount + len(body.nodes)) >= nodelimit: if (nodecount + len(body.nodes)) > nodelimit:
raise ClientSideError( raise OverLimit(
'Command would exceed Load Balancer node limit' 'Command would exceed Load Balancer node limit'
) )
return_data = LBNodeResp() return_data = LBNodeResp()
@ -143,7 +161,7 @@ class NodesController(RestController):
enabled = 1 enabled = 1
new_node = Node( new_node = Node(
lbid=self.lbid, port=node.port, address=node.address, 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.add(new_node)
session.flush() session.flush()
@ -206,7 +224,7 @@ class NodesController(RestController):
submit_job( submit_job(
'UPDATE', device.name, device.id, lb.id 'UPDATE', device.name, device.id, lb.id
) )
return return ''
@expose('json') @expose('json')
def delete(self, node_id): def delete(self, node_id):

27
libra/api/library/exp.py Normal file
View 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)

View File

@ -107,7 +107,8 @@ class GearmanClientThread(object):
# Device should never be used again # Device should never be used again
device = session.query(Device).\ device = session.query(Device).\
filter(Device.id == data).first() filter(Device.id == data).first()
device.status = 'DELETED' #TODO: change this to 'DELETED' when pool mgm deletes
device.status = 'OFFLINE'
session.commit() session.commit()
def _set_error(self, device_id, errmsg): def _set_error(self, device_id, errmsg):

View File

@ -24,6 +24,12 @@ class LBNode(Base):
condition = Enum(wtypes.text, 'ENABLED', 'DISABLED') condition = Enum(wtypes.text, 'ENABLED', 'DISABLED')
class LBRespNode(Base):
port = wtypes.text
address = wtypes.text
condition = wtypes.text
class LBNodePut(Base): class LBNodePut(Base):
condition = Enum(wtypes.text, 'ENABLED', 'DISABLED') condition = Enum(wtypes.text, 'ENABLED', 'DISABLED')
@ -63,7 +69,7 @@ class LBPut(Base):
class LBVipResp(Base): class LBVipResp(Base):
id = int id = wtypes.text
address = wtypes.text address = wtypes.text
type = wtypes.text type = wtypes.text
ipVersion = wtypes.text ipVersion = wtypes.text
@ -77,13 +83,13 @@ class LBLogsPost(Base):
class LBResp(Base): class LBResp(Base):
id = int id = wtypes.text
name = wtypes.text name = wtypes.text
protocol = wtypes.text protocol = wtypes.text
port = int port = wtypes.text
algorithm = wtypes.text algorithm = wtypes.text
status = wtypes.text status = wtypes.text
created = wtypes.text created = wtypes.text
updated = wtypes.text updated = wtypes.text
virtualIps = wsattr(['LBVipResp']) virtualIps = wsattr(['LBVipResp'])
nodes = wsattr(['LBNode']) nodes = wsattr(['LBRespNode'])

118
libra/api/wsme_overrides.py Normal file
View 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