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
This commit is contained in:
Andrew Hutchings 2013-05-31 20:37:06 +01:00
parent cbe623c226
commit 1c490c7bfb
9 changed files with 161 additions and 313 deletions

View File

@ -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.

View File

@ -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.

View File

@ -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

View File

@ -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):

View File

@ -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):

View File

@ -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.

View File

@ -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()

View File

@ -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'

View File

@ -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'
}
]
}