diff --git a/libra/api/app.py b/libra/api/app.py index bf0d20c4..4b5ad627 100644 --- a/libra/api/app.py +++ b/libra/api/app.py @@ -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') diff --git a/libra/api/config.py b/libra/api/config.py index fe42ca4b..7fe69270 100644 --- a/libra/api/config.py +++ b/libra/api/config.py @@ -26,9 +26,9 @@ app = { } } -wsme = { - 'debug': True -} +#wsme = { +# 'debug': True +#} #database = { # 'username': 'root', diff --git a/libra/api/controllers/load_balancers.py b/libra/api/controllers/load_balancers.py index 87c435ab..ab93e88b 100644 --- a/libra/api/controllers/load_balancers.py +++ b/libra/api/controllers/load_balancers.py @@ -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') diff --git a/libra/api/controllers/nodes.py b/libra/api/controllers/nodes.py index 3a551b85..75cfe413 100644 --- a/libra/api/controllers/nodes.py +++ b/libra/api/controllers/nodes.py @@ -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): diff --git a/libra/api/library/exp.py b/libra/api/library/exp.py new file mode 100644 index 00000000..d499a3b2 --- /dev/null +++ b/libra/api/library/exp.py @@ -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) diff --git a/libra/api/library/gearman_client.py b/libra/api/library/gearman_client.py index 88d608a6..8e35cd33 100644 --- a/libra/api/library/gearman_client.py +++ b/libra/api/library/gearman_client.py @@ -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): diff --git a/libra/api/model/validators.py b/libra/api/model/validators.py index b88c400e..fd90cc8f 100644 --- a/libra/api/model/validators.py +++ b/libra/api/model/validators.py @@ -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']) diff --git a/libra/api/wsme_overrides.py b/libra/api/wsme_overrides.py new file mode 100644 index 00000000..a4ae7ead --- /dev/null +++ b/libra/api/wsme_overrides.py @@ -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