libra/libra/api/controllers/load_balancers.py
Andrew Hutchings d5b5ea56dc API: Add support for ARCHIVE feature
Also add missing file for virtualips support

Change-Id: Iaa5c74047360d7602a0efd6ab7436193c65b8419
2013-06-07 14:38:18 +01:00

434 lines
15 KiB
Python

# 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
# 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, InvalidInput
from wsme import Unset
# other controllers
from nodes import NodesController
from virtualips import VipsController
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.library.gearman_client import submit_job
from libra.api.acl import get_limited_to_project
class LoadBalancersController(RestController):
def __init__(self, lbid=None):
self.lbid = lbid
@expose('json')
def get(self, load_balancer_id=None):
"""Fetches a list of load balancers or the details of one balancer if
load_balancer_id is not empty.
:param load_balancer_id: id of lb we want to get, if none it returns a
list of all
Url:
GET /loadbalancers
List all load balancers configured for the account.
Url:
GET /loadbalancers/{load_balancer_id}
List details of the specified load balancer.
Returns: dict
"""
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:
load_balancers = {'loadBalancers': 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()}
else:
load_balancers = session.query(
LoadBalancer.name, LoadBalancer.id, LoadBalancer.protocol,
LoadBalancer.port, LoadBalancer.algorithm,
LoadBalancer.status, LoadBalancer.created,
LoadBalancer.updated, LoadBalancer.statusDescription
).join(LoadBalancer.devices).\
filter(LoadBalancer.tenantid == tenant_id).\
filter(LoadBalancer.id == load_balancer_id).\
first()
if not load_balancers:
response.status = 400
session.rollback()
return dict(
faultcode='Client',
faultstring="Load Balancer ID not found"
)
load_balancers = load_balancers._asdict()
virtualIps = session.query(
Device.id, Device.floatingIpAddr
).join(LoadBalancer.devices).\
filter(LoadBalancer.tenantid == tenant_id).\
filter(LoadBalancer.id == load_balancer_id).\
all()
load_balancers['virtualIps'] = []
for item in virtualIps:
vip = item._asdict()
vip['type'] = 'PUBLIC'
vip['ipVersion'] = 'IPV4'
load_balancers['virtualIps'].append(vip)
nodes = session.query(
Node.id, Node.address, Node.port, Node.status, Node.enabled
).join(LoadBalancer.nodes).\
filter(LoadBalancer.tenantid == tenant_id).\
filter(LoadBalancer.id == load_balancer_id).\
all()
load_balancers['nodes'] = []
for item in nodes:
node = item._asdict()
if node['enabled'] == 1:
node['condition'] = 'ENABLED'
else:
node['condition'] = 'DISABLED'
del node['enabled']
load_balancers['nodes'].append(node)
session.commit()
response.status = 200
return load_balancers
@wsme_pecan.wsexpose(LBResp, body=LBPost, status_code=202)
def post(self, body=None):
"""Accepts edit if load_balancer_id isn't blank or create load balancer
posts.
:param load_balancer_id: id of lb
:param *args: holds the posted json or xml data
Urls:
POST /loadbalancers/{load_balancer_id}
PUT /loadbalancers
Notes:
curl -i -H "Accept: application/json" -X POST \
-d "data={"name": "my_lb"}" \
http://dev.server:8080/loadbalancers/100
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).\
filter(LoadBalancer.status != 'DELETED').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)
)
device = None
old_lb = None
# if we don't have an id then we want to create a new lb
lb = LoadBalancer()
if body.virtualIps == Unset:
# find free device
device = session.query(Device).\
filter(~Device.id.in_(
session.query(loadbalancers_devices.c.device)
)).\
filter(Device.status == "OFFLINE").\
first()
else:
virtual_id = body.virtualIps[0].id
# This is an additional load balancer
device = session.query(
Device
).filter(Device.id == virtual_id).\
first()
old_lb = session.query(
LoadBalancer
).join(LoadBalancer.devices).\
filter(LoadBalancer.tenantid == tenant_id).\
filter(Device.id == virtual_id).\
first()
if old_lb is None:
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:
raise RuntimeError('No devices available')
lb.tenantid = tenant_id
lb.name = body.name
if body.protocol and body.protocol.lower() == 'tcp':
lb.protocol = 'TCP'
else:
lb.protocol = 'HTTP'
if body.port:
lb.port = body.port
else:
if lb.protocol == 'HTTP':
lb.port = 80
else:
lb.port = 443
lb.status = 'BUILD'
lb.created = None
if body.algorithm:
lb.algorithm = body.algorithm.upper()
else:
lb.algorithm = 'ROUND_ROBIN'
lb.devices = [device]
# write to database
session.add(lb)
session.flush()
#refresh the lb record so we get the id back
session.refresh(lb)
for node in body.nodes:
if node.condition == 'DISABLED':
enabled = 0
else:
enabled = 1
out_node = Node(
lbid=lb.id, port=node.port, address=node.address,
enabled=enabled, status='ONLINE', weight=0
)
session.add(out_node)
# now save the loadbalancer_id to the device and switch its status
# to online
device.status = "ONLINE"
session.flush()
try:
return_data = LBResp()
return_data.id = lb.id
return_data.name = lb.name
return_data.protocol = lb.protocol
return_data.port = 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,
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,
condition=node.condition
)
return_data.nodes.append(out_node)
session.commit()
# trigger gearman client to create new lb
result = submit_job(
'UPDATE', device.name, device.id, lb.id
)
# do something with result
if result:
pass
return return_data
except:
logger = logging.getLogger(__name__)
logger.exception('Error communicating with load balancer pool')
errstr = 'Error communicating with load balancer pool'
session.rollback()
raise ClientSideError(errstr)
@wsme_pecan.wsexpose(None, body=LBPut, status_code=202)
def put(self, body=None):
if not self.lbid:
raise ClientSideError('Load Balancer ID is required')
tenant_id = get_limited_to_project(request.headers)
# grab the lb
lb = session.query(LoadBalancer).\
filter(LoadBalancer.id == self.lbid).\
filter(LoadBalancer.tenantid == tenant_id).\
filter(LoadBalancer.status != 'DELETED').first()
if lb is None:
raise ClientSideError('Load Balancer ID is not valid')
if body.name != Unset:
lb.name = body.name
if body.algorithm != Unset:
lb.algorithm = body.algorithm
lb.status = 'PENDING_UPDATE'
device = session.query(
Device.id, Device.name
).join(LoadBalancer.devices).\
filter(LoadBalancer.id == self.lbid).\
first()
session.commit()
submit_job(
'UPDATE', device.name, device.id, lb.id
)
return
@expose('json')
def delete(self, load_balancer_id):
"""Remove a load balancer from the account.
:param load_balancer_id: id of lb
Urls:
DELETE /loadbalancers/{load_balancer_id}
Notes:
curl -i -H "Accept: application/json" -X DELETE
http://dev.server:8080/loadbalancers/1
Returns: None
"""
tenant_id = get_limited_to_project(request.headers)
# grab the lb
lb = session.query(LoadBalancer).\
filter(LoadBalancer.id == load_balancer_id).\
filter(LoadBalancer.tenantid == tenant_id).\
filter(LoadBalancer.status != 'DELETED').first()
if lb is None:
response.status = 400
return dict(
faultcode="Client",
faultstring="Load Balancer ID is not valid"
)
try:
session.query(Node).filter(Node.lbid == load_balancer_id).delete()
lb.status = 'PENDING_DELETE'
device = session.query(
Device.id, Device.name
).join(LoadBalancer.devices).\
filter(LoadBalancer.id == load_balancer_id).\
first()
session.execute(loadbalancers_devices.delete().where(
loadbalancers_devices.c.loadbalancer == load_balancer_id
))
session.flush()
session.commit()
submit_job(
'DELETE', device.name, device.id, lb.id
)
response.status = 202
return None
except:
logger = logging.getLogger(__name__)
logger.exception('Error communicating with load balancer pool')
response.status = 500
return dict(
faultcode="Server",
faultstring="Error communication with load balancer pool"
)
def usage(self, load_balancer_id):
"""List current and historical usage.
:param load_balancer_id: id of lb
Url:
GET /loadbalancers/{load_balancer_id}/usage
Returns: dict
"""
response.status = 201
return None
@expose('json')
def _lookup(self, lbid, *remainder):
"""Routes more complex url mapping.
Most things are /loadbalancer/{id}/function/... so this routes that
Raises: 404
"""
if len(remainder):
if remainder[0] == 'nodes':
return NodesController(lbid), remainder[1:]
if remainder[0] == 'virtualips':
return VipsController(lbid), remainder[1:]
if remainder[0] == 'logs':
return LogsController(lbid), remainder[1:]
# Kludgy fix for PUT since WSME doesn't like IDs on the path
elif lbid:
return LoadBalancersController(lbid), remainder
abort(404)