Andrew Hutchings df060ccd19 API+ADMIN_API: SSL and DB fixes
* Add SSL to Admin API
* Improve debug flag on both APIs
* Make Admin API use its own model
* Improve DB session handling and rollbacks

Change-Id: I9d268079effbaca70c8c69d591bf48d494c1293f
2013-06-10 14:19:56 +01:00

317 lines
11 KiB
Python

# 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.
import socket
from pecan import expose, response, request, abort
from pecan.rest import RestController
import wsmeext.pecan as wsme_pecan
from wsme.exc import ClientSideError
from wsme import Unset
#default response objects
from libra.api.model.lbaas import LoadBalancer, Node, get_session, Limits
from libra.api.model.lbaas import Device
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):
"""Functions for /loadbalancers/{load_balancer_id}/nodes/* routing"""
def __init__(self, lbid, nodeid=None):
self.lbid = lbid
self.nodeid = nodeid
@expose('json')
def get(self, node_id=None):
"""List node(s) configured for the load balancer OR if
node_id == None .. Retrieve the configuration of node {node_id} of
loadbalancer {load_balancer_id}.
:param load_balancer_id: id of lb
:param node_id: id of node (optional)
Urls:
GET /loadbalancers/{load_balancer_id}/nodes
GET /loadbalancers/{load_balancer_id}/nodes/{node_id}
Returns: dict
"""
tenant_id = get_limited_to_project(request.headers)
if not self.lbid:
response.status = 400
return dict(
faultcode='Client',
faultstring='Load Balancer ID not supplied'
)
session = get_session()
if not node_id:
nodes = session.query(
Node.id, Node.address, Node.port, Node.status, Node.enabled
).join(LoadBalancer.nodes).\
filter(LoadBalancer.tenantid == tenant_id).\
filter(LoadBalancer.id == self.lbid).\
filter(LoadBalancer.status != 'DELETED').\
all()
node_response = {'nodes': []}
for item in nodes:
node = item._asdict()
if node['enabled'] == 1:
node['condition'] = 'ENABLED'
else:
node['condition'] = 'DISABLED'
del node['enabled']
node_response['nodes'].append(node)
else:
node_response = session.query(
Node.id, Node.address, Node.port, Node.status, Node.enabled
).join(LoadBalancer.nodes).\
filter(LoadBalancer.tenantid == tenant_id).\
filter(LoadBalancer.id == self.lbid).\
filter(Node.id == node_id).\
first()
if node_response is None:
session.rollback()
response.status = 400
return dict(faultcode='Client', faultstring='node not found')
else:
session.commit()
response.status = 200
return node_response
@wsme_pecan.wsexpose(LBNodeResp, body=LBNodePost, status_code=202)
def post(self, body=None):
"""Adds a new node to the load balancer OR Modify the configuration
of a node on the load balancer.
:param load_balancer_id: id of lb
:param node_id: id of node (optional) when missing a new node is added.
:param *args: holds the posted json or xml data
Urls:
POST /loadbalancers/{load_balancer_id}/nodes
PUT /loadbalancers/{load_balancer_id}/nodes/{node_id}
Returns: dict of the full list of nodes or the details of the single
node
"""
tenant_id = get_limited_to_project(request.headers)
if self.lbid is None:
raise ClientSideError('Load Balancer ID has not been supplied')
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)
)
session = get_session()
load_balancer = session.query(LoadBalancer).\
filter(LoadBalancer.tenantid == tenant_id).\
filter(LoadBalancer.id == self.lbid).\
filter(LoadBalancer.status != 'DELETED').\
first()
if load_balancer is None:
session.rollback()
raise ClientSideError('Load Balancer not found')
load_balancer.status = 'PENDING_UPDATE'
# check if we are over limit
nodelimit = session.query(Limits.value).\
filter(Limits.name == 'maxNodesPerLoadBalancer').scalar()
nodecount = session.query(Node).\
filter(Node.lbid == self.lbid).count()
if (nodecount + len(body.nodes)) > nodelimit:
session.rollback()
raise OverLimit(
'Command would exceed Load Balancer node limit'
)
return_data = LBNodeResp()
return_data.nodes = []
for node in body.nodes:
if node.condition == 'DISABLED':
enabled = 0
else:
enabled = 1
new_node = Node(
lbid=self.lbid, port=node.port, address=node.address,
enabled=enabled, status='ONLINE', weight=1
)
session.add(new_node)
session.flush()
if new_node.enabled:
condition = 'ENABLED'
else:
condition = 'DISABLED'
return_data.nodes.append(
NodeResp(
id=new_node.id, port=new_node.port,
address=new_node.address, condition=condition,
status='ONLINE'
)
)
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, self.lbid
)
return return_data
@wsme_pecan.wsexpose(None, body=LBNodePut, status_code=202)
def put(self, body=None):
if not self.lbid:
raise ClientSideError('Load Balancer ID has not been supplied')
if not self.nodeid:
raise ClientSideError('Node ID has not been supplied')
tenant_id = get_limited_to_project(request.headers)
session = get_session()
# 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:
session.rollback()
raise ClientSideError('Load Balancer ID is not valid')
node = session.query(Node).\
filter(Node.lbid == self.lbid).\
filter(Node.id == self.nodeid).first()
if node is None:
session.rollback()
raise ClientSideError('Node ID is not valid')
if body.condition != Unset:
if body.condition == 'DISABLED':
node.enabled = 0
else:
node.enabled = 1
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, node_id):
"""Remove a node from the load balancer.
:param load_balancer_id: id of lb
:param node_id: id of node
Url:
DELETE /loadbalancers/{load_balancer_id}/nodes/{node_id}
Returns: None
"""
tenant_id = get_limited_to_project(request.headers)
if self.lbid is None:
response.status = 400
return dict(
faultcode="Client",
faultstring='Load Balancer ID has not been supplied'
)
tenant_id = get_limited_to_project(request.headers)
session = get_session()
load_balancer = session.query(LoadBalancer).\
filter(LoadBalancer.tenantid == tenant_id).\
filter(LoadBalancer.id == self.lbid).\
filter(LoadBalancer.device != 'DELETED').\
first()
if load_balancer is None:
session.rollback()
response.status = 400
return dict(
faultcode="Client",
faultstring="Load Balancer not found"
)
load_balancer.status = 'PENDING_UPDATE'
nodecount = session.query(Node).\
filter(Node.lbid == self.lbid).count()
# Can't delete the last LB
if nodecount <= 1:
session.rollback()
response.status = 400
return dict(
faultcode="Client",
faultstring="Cannot delete the last node in a load balancer"
)
node = session.query(Node).\
filter(Node.lbid == self.lbid).\
filter(Node.id == node_id).\
first()
if not node:
session.rollback()
response.status = 400
return dict(
faultcode="Client",
faultstring="Node not found in supplied Load Balancer"
)
session.delete(node)
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, self.lbid
)
response.status = 202
return None
@expose('json')
def _lookup(self, nodeid, *remainder):
"""Routes more complex url mapping.
Raises: 404
"""
# Kludgy fix for PUT since WSME doesn't like IDs on the path
if nodeid:
return NodesController(self.lbid, nodeid), remainder
abort(404)