From 1c490c7bfbf36a92e2b9cb33dc89bc867824b6b5 Mon Sep 17 00:00:00 2001 From: Andrew Hutchings Date: Fri, 31 May 2013 20:37:06 +0100 Subject: [PATCH] API: More work on the API server * Improved error handling * VirtualIPs function added * Limits function added * Limits checked in create * Other cleanups/fixes Change-Id: Idf5abc1d9a568d8193aee9bc7b60c3224c8c331c --- libra/api/controllers/connection_throttle.py | 11 +- libra/api/controllers/health_monitor.py | 11 +- libra/api/controllers/limits.py | 30 +++ libra/api/controllers/load_balancers.py | 160 +++++++++---- libra/api/controllers/nodes.py | 3 +- libra/api/controllers/session_persistence.py | 10 +- libra/api/controllers/v1.py | 2 + libra/api/model/lbaas.py | 7 + libra/api/model/responses.py | 240 ------------------- 9 files changed, 161 insertions(+), 313 deletions(-) create mode 100644 libra/api/controllers/limits.py diff --git a/libra/api/controllers/connection_throttle.py b/libra/api/controllers/connection_throttle.py index 868505f2..5682302e 100644 --- a/libra/api/controllers/connection_throttle.py +++ b/libra/api/controllers/connection_throttle.py @@ -13,17 +13,14 @@ # License for the specific language governing permissions and limitations # under the License. -from pecan import expose, response +from pecan import response from pecan.rest import RestController -from libra.api.model.responses import Responses - class ConnectionThrottleController(RestController): """functions for /loadbalancers/{loadBalancerId}/connectionthrottle/* routing""" - @expose('json') def get(self, load_balancer_id): """List connection throttling configuration. @@ -35,9 +32,8 @@ class ConnectionThrottleController(RestController): Returns: dict """ response.status = 201 - return Responses.LoadBalancers.ConnectionThrottle.get + return None - @expose('json') def post(self, load_balancer_id, *args): """Update throttling configuration. @@ -50,9 +46,8 @@ class ConnectionThrottleController(RestController): Returns: dict """ response.status = 201 - return Responses.LoadBalancers.ConnectionThrottle.get + return None - @expose() def delete(self, loadbalancer_id): """Remove connection throttling configurations. diff --git a/libra/api/controllers/health_monitor.py b/libra/api/controllers/health_monitor.py index 8affea49..740b3c8e 100644 --- a/libra/api/controllers/health_monitor.py +++ b/libra/api/controllers/health_monitor.py @@ -13,16 +13,13 @@ # License for the specific language governing permissions and limitations # under the License. -from pecan import expose, response +from pecan import response from pecan.rest import RestController -from libra.api.model.responses import Responses - class HealthMonitorController(RestController): """functions for /loadbalancers/{loadBalancerId}/healthmonitor/* routing""" - @expose('json') def get(self, load_balancer_id): """Retrieve the health monitor configuration, if one exists. @@ -34,9 +31,8 @@ class HealthMonitorController(RestController): Returns: dict """ response.status = 201 - return Responses.LoadBalancers.get + return None - @expose('json') def post(self, load_balancer_id, *args): """Update the settings for a health monitor. @@ -49,9 +45,8 @@ class HealthMonitorController(RestController): Returns: dict """ response.status = 201 - return Responses.LoadBalancers.get + return None - @expose() def delete(self, load_balancer_id): """Remove the health monitor. diff --git a/libra/api/controllers/limits.py b/libra/api/controllers/limits.py new file mode 100644 index 00000000..25969bd2 --- /dev/null +++ b/libra/api/controllers/limits.py @@ -0,0 +1,30 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# 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. + +from pecan import expose +from pecan.rest import RestController +from libra.api.model.lbaas import Limits, session + + +class LimitsController(RestController): + @expose('json') + def get(self): + resp = {} + limits = session.query(Limits).all() + for limit in limits: + resp[limit.name] = limit.value + + resp = {"limits": {"absolute": {"values": resp}}} + return resp diff --git a/libra/api/controllers/load_balancers.py b/libra/api/controllers/load_balancers.py index 636558a1..458bb4af 100644 --- a/libra/api/controllers/load_balancers.py +++ b/libra/api/controllers/load_balancers.py @@ -1,4 +1,3 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 # Copyright 2013 Hewlett-Packard Development Company, L.P. # # Licensed under the Apache License, Version 2.0 (the "License"); you may @@ -13,41 +12,27 @@ # License for the specific language governing permissions and limitations # under the License. -#import gearman.errors import logging # pecan imports from pecan import expose, abort, response, request from pecan.rest import RestController import wsmeext.pecan as wsme_pecan -from wsme.exc import ClientSideError +from wsme.exc import ClientSideError, InvalidInput +from wsme import Unset # other controllers from nodes import NodesController from health_monitor import HealthMonitorController from session_persistence import SessionPersistenceController from connection_throttle import ConnectionThrottleController -#from sqlalchemy.orm import aliased -# default response objects -from libra.api.model.responses import Responses # models from libra.api.model.lbaas import LoadBalancer, Device, Node, session -from libra.api.model.lbaas import loadbalancers_devices +from libra.api.model.lbaas import loadbalancers_devices, Limits from libra.api.model.validators import LBPost, LBResp, LBVipResp, LBNode from libra.api.library.gearman_client import submit_job from libra.api.acl import get_limited_to_project class LoadBalancersController(RestController): - """functions for /loadbalancer routing""" - loadbalancer_status = ( - 'ACTIVE', - 'BUILD', - 'PENDING_UPDATE', - 'PENDING_DELETE', - 'DELETED', - 'SUSPENDED', - 'ERROR' - ) - """nodes subclass linking controller class for urls that look like /loadbalancers/{loadBalancerId}/nodes/* @@ -73,7 +58,7 @@ class LoadBalancersController(RestController): connectionthrottle = ConnectionThrottleController() @expose('json') - def get(self, load_balancer_id=None): + def get(self, load_balancer_id=None, command=None): """Fetches a list of load balancers or the details of one balancer if load_balancer_id is not empty. @@ -90,11 +75,15 @@ class LoadBalancersController(RestController): Returns: dict """ + if command == 'virtualips': + return self.virtualips(load_balancer_id) + elif command: + abort(404) + tenant_id = get_limited_to_project(request.headers) # if we don't have an id then we want a list of them own by this tenent if not load_balancer_id: - #return Responses.LoadBalancers.get load_balancers = {'loadBalancers': session.query( LoadBalancer.name, LoadBalancer.id, LoadBalancer.protocol, LoadBalancer.port, LoadBalancer.algorithm, @@ -102,7 +91,6 @@ class LoadBalancersController(RestController): LoadBalancer.updated ).filter_by(tenantid=tenant_id).all()} else: - #return Responses.LoadBalancers.detail load_balancers = session.query( LoadBalancer.name, LoadBalancer.id, LoadBalancer.protocol, LoadBalancer.port, LoadBalancer.algorithm, @@ -116,7 +104,10 @@ class LoadBalancersController(RestController): if not load_balancers: response.status = 400 session.rollback() - return dict(status=400, message="load balancer not found") + return dict( + faultcode='Client', + faultstring="Load Balancer ID not found" + ) load_balancers = load_balancers._asdict() virtualIps = session.query( @@ -174,9 +165,30 @@ class LoadBalancersController(RestController): Returns: dict """ tenant_id = get_limited_to_project(request.headers) + if body.nodes == Unset: + raise ClientSideError( + 'At least one backend node needs to be supplied' + ) + + lblimit = session.query(Limits.value).\ + filter(Limits.name == 'maxLoadBalancers').scalar() + nodelimit = session.query(Limits.value).\ + filter(Limits.name == 'maxNodesPerLoadBalancer').scalar() + count = session.query(LoadBalancer).\ + filter(LoadBalancer.tenantid == tenant_id).count() + + # TODO: this should probably be a 413, not sure how to do that yet + if count >= lblimit: + raise ClientSideError( + 'Account has hit limit of {0} Load Balancers'. + format(lblimit) + ) + if len(body.nodes) > nodelimit: + raise ClientSideError( + 'Too many backend nodes supplied (limit is {0}'. + format(nodelimit) + ) - # TODO: check if tenant is overlimit (return a 413 for that) - # TODO: check if we have been supplied with too many nodes device = None old_lb = None # if we don't have an id then we want to create a new lb @@ -203,22 +215,39 @@ class LoadBalancersController(RestController): filter(Device.id == virtual_id).\ first() if old_lb is None: - response.status = 400 - return Responses.service_unavailable - if old_lb.protocol == 'HTTP' and ( - body.protocol is None or body.protocol == 'HTTP' - ): - # Error here, can have only one HTTP - response.status = 400 - return Responses.service_unavailable - elif old_lb.protocol == 'TCP' and body.protocol == 'TCP': - # Error here, can have only one TCP - response.status = 400 - return Responses.service_unavailable + raise InvalidInput( + 'virtualIps', virtual_id, 'Invalid virtual IP provided' + ) + + if body.protocol == Unset or body.protocol.lower() == 'HTTP': + old_count = session.query( + LoadBalancer + ).join(LoadBalancer.devices).\ + filter(LoadBalancer.tenantid == tenant_id).\ + filter(Device.id == virtual_id).\ + filter(LoadBalancer.protocol == 'HTTP').\ + count() + if old_count: + # Error here, can have only one HTTP + raise ClientSideError( + 'Only one HTTP load balancer allowed per device' + ) + elif body.protocol.lower() == 'TCP': + old_count = session.query( + LoadBalancer + ).join(LoadBalancer.devices).\ + filter(LoadBalancer.tenantid == tenant_id).\ + filter(Device.id == virtual_id).\ + filter(LoadBalancer.protocol == 'TCP').\ + count() + if old_count: + # Error here, can have only one TCP + raise ClientSideError( + 'Only one TCP load balancer allowed per device' + ) if device is None: - response.status = 503 - return Responses.service_unavailable + raise RuntimeError('No devices available') lb.tenantid = tenant_id lb.name = body.name @@ -265,7 +294,6 @@ class LoadBalancersController(RestController): device.status = "ONLINE" session.flush() - # TODO: write nodes to table too job_data = { 'hpcs_action': 'UPDATE', @@ -356,6 +384,8 @@ class LoadBalancersController(RestController): Returns: None """ + # TODO: send gearman message (use PENDING_DELETE), make it an update + # message when more than one device per LB tenant_id = get_limited_to_project(request.headers) # grab the lb lb = session.query(LoadBalancer).\ @@ -364,8 +394,10 @@ class LoadBalancersController(RestController): if lb is None: response.status = 400 - return Responses.not_found - + return dict( + faultcode="Client", + faultstring="Load Balancer ID is not valid" + ) try: session.query(Node).filter(Node.lbid == load_balancer_id).delete() lb.status = 'DELETED' @@ -377,21 +409,25 @@ class LoadBalancersController(RestController): session.execute(loadbalancers_devices.delete().where( loadbalancers_devices.c.loadbalancer == load_balancer_id )) - device.status = 'OFFLINE' + if device: + device.status = 'OFFLINE' session.flush() # trigger gearman client to create new lb #result = gearman_client.submit_job('DELETE', lb.output_to_json()) - response.status = 200 + response.status = 202 session.commit() - return self.get() + return None except: logger = logging.getLogger(__name__) logger.exception('Error communicating with load balancer pool') - response.status = 503 - return Responses.service_unavailable + response.status = 500 + return dict( + faultcode="Server", + faultstring="Error communication with load balancer pool" + ) def virtualips(self, load_balancer_id): """Returns a list of virtual ips attached to a specific Load Balancer. @@ -403,7 +439,35 @@ class LoadBalancersController(RestController): Returns: dict """ - return Responses.LoadBalancers.virtualips + tenant_id = get_limited_to_project(request.headers) + if not load_balancer_id: + response.status = 400 + return dict( + faultcode="Client", + faultstring="Load Balancer ID not provided" + ) + device = session.query( + Device.id, Device.floatingIpAddr + ).join(LoadBalancer.devices).\ + filter(LoadBalancer.id == load_balancer_id).\ + filter(LoadBalancer.tenantid == tenant_id).first() + + if not device: + response.status = 400 + return dict( + faultcode="Client", + faultstring="Load Balancer ID not valid" + ) + resp = { + "virtualIps": [{ + "id": device.id, + "address": device.floatingIpAddr, + "type": "PUBLIC", + "ipVersion": "IPV4" + }] + } + + return resp def usage(self, load_balancer_id): """List current and historical usage. @@ -416,7 +480,7 @@ class LoadBalancersController(RestController): Returns: dict """ response.status = 201 - return Responses.LoadBalancers.usage + return None @expose('json') def _lookup(self, primary_key, *remainder): diff --git a/libra/api/controllers/nodes.py b/libra/api/controllers/nodes.py index 073fa8b3..cf4267bb 100644 --- a/libra/api/controllers/nodes.py +++ b/libra/api/controllers/nodes.py @@ -17,7 +17,6 @@ from pecan import expose, response, request from pecan.rest import RestController #default response objects from libra.api.model.lbaas import LoadBalancer, Node, session -from libra.api.model.responses import Responses from libra.api.acl import get_limited_to_project @@ -97,7 +96,7 @@ class NodesController(RestController): node """ response.status = 201 - return Responses.LoadBalancers.Nodes.get + return None @expose() def delete(self, load_balancer_id, node_id): diff --git a/libra/api/controllers/session_persistence.py b/libra/api/controllers/session_persistence.py index efe79d01..808d639f 100644 --- a/libra/api/controllers/session_persistence.py +++ b/libra/api/controllers/session_persistence.py @@ -13,9 +13,8 @@ # License for the specific language governing permissions and limitations # under the License. -from pecan import expose, response +from pecan import response from pecan.rest import RestController -from libra.api.model.responses import Responses class SessionPersistenceController(RestController): @@ -23,7 +22,6 @@ class SessionPersistenceController(RestController): functions for /loadbalancers/{loadBalancerId}/sessionpersistence/* routing """ - @expose('json') def get(self, load_balancer_id): """List session persistence configuration.get @@ -35,9 +33,8 @@ class SessionPersistenceController(RestController): Returns: dict """ response.status = 201 - return Responses.LoadBalancers.SessionPersistence.get + return None - @expose('json') def post(self, load_balancer_id): """Enable session persistence. @@ -49,9 +46,8 @@ class SessionPersistenceController(RestController): Returns: dict """ response.status = 201 - return Responses.LoadBalancers.SessionPersistence.get + return None - @expose('json') def delete(self, load_balancer_id): """Disable session persistence. diff --git a/libra/api/controllers/v1.py b/libra/api/controllers/v1.py index 8045224f..d94b237c 100644 --- a/libra/api/controllers/v1.py +++ b/libra/api/controllers/v1.py @@ -15,6 +15,7 @@ from pecan import expose, response from load_balancers import LoadBalancersController +from limits import LimitsController from libra.api.model.responses import Responses @@ -52,3 +53,4 @@ class V1Controller(object): #pecan uses this controller class for urls that start with /loadbalancers loadbalancers = LoadBalancersController() + limits = LimitsController() diff --git a/libra/api/model/lbaas.py b/libra/api/model/lbaas.py index d156c41a..778549ac 100644 --- a/libra/api/model/lbaas.py +++ b/libra/api/model/lbaas.py @@ -50,6 +50,13 @@ class FormatedDateTime(types.TypeDecorator): return value.strftime('%Y-%m-%dT%H:%M:%S') +class Limits(DeclarativeBase): + __tablename__ = 'global_limits' + id = Column(u'id', Integer, primary_key=True, nullable=False) + name = Column(u'name', VARCHAR(length=128), nullable=False) + value = Column(u'value', BIGINT(), nullable=False) + + class Device(DeclarativeBase): """device model""" __tablename__ = 'devices' diff --git a/libra/api/model/responses.py b/libra/api/model/responses.py index c8d1a79a..64d90e66 100644 --- a/libra/api/model/responses.py +++ b/libra/api/model/responses.py @@ -88,243 +88,3 @@ class Responses(object): ] } } - """class LoadBalancers - grouping of lb responses - """ - - class LoadBalancers(object): - """LoadBalancers list""" - get = { - 'loadBalancers': [ - { - 'name': 'lb-site1', - 'id': '71', - 'protocol': 'HTTP', - 'port': '80', - 'algorithm': 'LEAST_CONNECTIONS', - 'status': 'ACTIVE', - 'created': '2010-11-30T03:23:42Z', - 'updated': '2010-11-30T03:23:44Z' - }, - { - 'name': 'lb-site2', - 'id': '166', - 'protocol': 'TCP', - 'port': '9123', - 'algorithm': 'ROUND_ROBIN', - 'status': 'ACTIVE', - 'created': '2010-11-30T03:23:42Z', - 'updated': '2010-11-30T03:23:44Z' - } - ] - } - - """loadbalancer details""" - detail = { - 'id': '2000', - 'name': 'sample-loadbalancer', - 'protocol': 'HTTP', - 'port': '80', - 'algorithm': 'ROUND_ROBIN', - 'status': 'ACTIVE', - 'created': '2010-11-30T03:23:42Z', - 'updated': '2010-11-30T03:23:44Z', - 'virtualIps': [ - { - 'id': '1000', - 'address': '2001:cdba:0000:0000:0000:0000:3257:9652', - 'type': 'PUBLIC', - 'ipVersion': 'IPV6' - } - ], - 'nodes': [ - { - 'id': '1041', - 'address': '10.1.1.1', - 'port': '80', - 'condition': 'ENABLED', - 'status': 'ONLINE' - }, - { - 'id': '1411', - 'address': '10.1.1.2', - 'port': '80', - 'condition': 'ENABLED', - 'status': 'ONLINE' - } - ], - 'sessionPersistence': { - 'persistenceType': 'HTTP_COOKIE' - }, - 'connectionThrottle': { - 'maxRequestRate': '50', - 'rateInterval': '60' - } - } - - """create loadbalancer response""" - post = { - 'name': 'a-new-loadbalancer', - 'id': '144', - 'protocol': 'HTTP', - 'port': '83', - 'algorithm': 'ROUND_ROBIN', - 'status': 'BUILD', - 'created': '2011-04-13T14:18:07Z', - 'updated': '2011-04-13T14:18:07Z', - 'virtualIps': [ - { - 'address': '3ffe:1900:4545:3:200:f8ff:fe21:67cf', - 'id': '39', - 'type': 'PUBLIC', - 'ipVersion': 'IPV6' - } - ], - 'nodes': [ - { - 'address': '10.1.1.1', - 'id': '653', - 'port': '80', - 'status': 'ONLINE', - 'condition': 'ENABLED' - } - ] - } - - """virtualips""" - virtualips = { - 'virtualIps': [ - { - 'id': '1021', - 'address': '206.10.10.210', - 'type': 'PUBLIC', - 'ipVersion': 'IPV4' - } - ] - } - - """usage""" - usage = { - 'loadBalancerUsageRecords': [ - { - 'id': '394', - 'transferBytesIn': '2819204', - 'transferBytesOut': '84923069' - }, - { - 'id': '473', - 'transferBytesIn': '0', - 'transferBytesOut': '0' - } - ] - } - - """class HealthMonitor - monitor responses - """ - - class HealthMonitor(object): - """monitor CONNECT response""" - get = { - 'type': 'CONNECT', - 'delay': '20', - 'timeout': '10', - 'attemptsBeforeDeactivation': '3' - } - """monitor HTTPS response""" - get_https = { - 'type': 'HTTPS', - 'delay': '10', - 'timeout': '3', - 'attemptsBeforeDeactivation': '3', - 'path': '/healthcheck' - } - """class SessionPersistence - for managing Session Persistance - """ - - class SessionPersistence(object): - """get""" - get = { - 'persistenceType': 'HTTP_COOKIE' - } - """class Connections - Throttle Connections responses - """ - - class ConnectionThrottle(object): - """get""" - get = { - 'maxRequestRate': '50', - 'rateInterval': '60' - } - - """class Nodes - grouping of node related responses - """ - - class Nodes(object): - """list of nodes of a specific lb""" - get = { - 'nodes': [ - { - 'id': '410', - 'address': '10.1.1.1', - 'port': '80', - 'condition': 'ENABLED', - 'status': 'ONLINE' - }, - { - 'id': '236', - 'address': '10.1.1.2', - 'port': '80', - 'condition': 'ENABLED', - 'status': 'ONLINE' - }, - { - 'id': '2815', - 'address': '10.1.1.3', - 'port': '83', - 'condition': 'DISABLED', - 'status': 'OFFLINE' - }, - ] - } - - """a specific node details""" - get_detail = { - 'id': '236', - 'address': '10.1.1.2', - 'port': '80', - 'condition': 'ENABLED', - 'status': 'ONLINE' - } - - """nodes create response""" - post = { - 'nodes': [ - { - 'id': '7298', - 'address': '10.1.1.1', - 'port': '80', - 'condition': 'ENABLED', - 'status': 'ONLINE' - }, - { - 'id': '293', - 'address': '10.2.2.1', - 'port': '80', - 'weight': '2', - 'condition': 'ENABLED', - 'status': 'OFFLINE' - }, - { - 'id': '183', - 'address': '10.2.2.4', - 'port': '88', - 'weight': '2', - 'condition': 'DISABLED', - 'status': 'OFFLINE' - } - ] - }