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
This commit is contained in:
Andrew Hutchings 2013-06-10 14:10:30 +01:00
parent 971f37d680
commit df060ccd19
14 changed files with 83 additions and 70 deletions

View File

@ -76,6 +76,8 @@ poll_timeout_retry = 30
db_user=root db_user=root
db_pass=passwd db_pass=passwd
db_schema=lbaas db_schema=lbaas
ssl_certfile=certfile.crt
ssl_keyfile=keyfile.key
[api] [api]
host=0.0.0.0 host=0.0.0.0

View File

@ -46,6 +46,10 @@ def setup_app(pecan_config, args):
'host': args.db_host, 'host': args.db_host,
'schema': args.db_schema 'schema': args.db_schema
} }
if args.debug:
config['wsme'] = {'debug': True}
config['app']['debug'] = True
pecan.configuration.set_config(config, overwrite=True) pecan.configuration.set_config(config, overwrite=True)
app = pecan.make_app( app = pecan.make_app(
@ -93,11 +97,20 @@ def main():
options.parser.add_argument( options.parser.add_argument(
'--db_schema', help='MySQL schema for libra' '--db_schema', help='MySQL schema for libra'
) )
options.parser.add_argument(
'--ssl_cert',
help='Path to an SSL certificate file'
)
options.parser.add_argument(
'--ssl_keyfile',
help='Path to an SSL key file'
)
args = options.run() args = options.run()
required_args = [ required_args = [
'db_user', 'db_pass', 'db_host', 'db_schema' 'db_user', 'db_pass', 'db_host', 'db_schema', 'ssl_certfile',
'ssl_keyfile'
] ]
missing_args = 0 missing_args = 0
@ -131,6 +144,15 @@ def main():
logger.info('Starting on {0}:{1}'.format(args.host, args.port)) logger.info('Starting on {0}:{1}'.format(args.host, args.port))
api = setup_app(pc, args) api = setup_app(pc, args)
sys.stderr = LogStdout(logger) sys.stderr = LogStdout(logger)
wsgi.server(eventlet.listen((args.host, args.port)), api) # TODO: set ca_certs and cert_reqs=CERT_REQUIRED
wsgi.server(
eventlet.wrap_ssl(
eventlet.listen((args.host, args.port)),
certfile=args.ssl_certfile,
keyfile=args.ssl_keyfile,
server_side=True
),
api
)
return 0 return 0

View File

@ -19,31 +19,8 @@ app = {
'modules': ['libra.admin_api'], 'modules': ['libra.admin_api'],
'static_root': '%(confdir)s/public', 'static_root': '%(confdir)s/public',
'template_path': '%(confdir)s/admin_api/templates', 'template_path': '%(confdir)s/admin_api/templates',
'debug': True,
'errors': { 'errors': {
404: '/notfound', 404: '/notfound',
'__force_dict__': True '__force_dict__': True
} }
} }
wsme = {
'debug': True
}
#database = {
# 'username': 'root',
# 'password': 'testaburger',
# 'host': 'localhost',
# 'schema': 'lbaas'
#}
#gearman = {
# 'server': ['localhost:4730'],
#}
# Custom Configurations must be in Python dictionary format::
#
# foo = {'bar':'baz'}
#
# All configurations are accessible at::
# pecan.conf

View File

@ -21,8 +21,8 @@ import wsmeext.pecan as wsme_pecan
from wsme.exc import ClientSideError from wsme.exc import ClientSideError
from usage import UsageController from usage import UsageController
from libra.admin_api.model.validators import DeviceResp, DevicePost, DevicePut from libra.admin_api.model.validators import DeviceResp, DevicePost, DevicePut
from libra.api.model.lbaas import LoadBalancer, Device, session from libra.admin_api.model.lbaas import LoadBalancer, Device, session
from libra.api.model.lbaas import loadbalancers_devices from libra.admin_api.model.lbaas import loadbalancers_devices
class DevicesController(RestController): class DevicesController(RestController):

View File

@ -14,7 +14,7 @@
# under the License. # under the License.
from pecan import expose, response from pecan import expose, response
from libra.api.model.lbaas import Device, session from libra.admin_api.model.lbaas import Device, session
from libra.admin_api.model.responses import Responses from libra.admin_api.model.responses import Responses
from pecan.rest import RestController from pecan.rest import RestController

View File

@ -59,6 +59,10 @@ def setup_app(pecan_config, args):
config['gearman'] = { config['gearman'] = {
'server': args.gearman 'server': args.gearman
} }
if args.debug:
config['wsme'] = {'debug': True}
config['app']['debug'] = True
pecan.configuration.set_config(config, overwrite=True) pecan.configuration.set_config(config, overwrite=True)
app = pecan.make_app( app = pecan.make_app(

View File

@ -19,31 +19,8 @@ app = {
'modules': ['libra.api'], 'modules': ['libra.api'],
'static_root': '%(confdir)s/public', 'static_root': '%(confdir)s/public',
'template_path': '%(confdir)s/api/templates', 'template_path': '%(confdir)s/api/templates',
'debug': True,
'errors': { 'errors': {
404: '/notfound', 404: '/notfound',
'__force_dict__': True '__force_dict__': True
} }
} }
#wsme = {
# 'debug': True
#}
#database = {
# 'username': 'root',
# 'password': 'testaburger',
# 'host': 'localhost',
# 'schema': 'lbaas'
#}
#gearman = {
# 'server': ['localhost:4730'],
#}
# Custom Configurations must be in Python dictionary format::
#
# foo = {'bar':'baz'}
#
# All configurations are accessible at::
# pecan.conf

View File

@ -15,16 +15,18 @@
from pecan import expose from pecan import expose
from pecan.rest import RestController from pecan.rest import RestController
from libra.api.model.lbaas import Limits, session from libra.api.model.lbaas import Limits, get_session
class LimitsController(RestController): class LimitsController(RestController):
@expose('json') @expose('json')
def get(self): def get(self):
resp = {} resp = {}
session = get_session()
limits = session.query(Limits).all() limits = session.query(Limits).all()
for limit in limits: for limit in limits:
resp[limit.name] = limit.value resp[limit.name] = limit.value
resp = {"limits": {"absolute": {"values": resp}}} resp = {"limits": {"absolute": {"values": resp}}}
session.rollback()
return resp return resp

View File

@ -25,7 +25,7 @@ from nodes import NodesController
from virtualips import VipsController from virtualips import VipsController
from logs import LogsController from logs import LogsController
# models # models
from libra.api.model.lbaas import LoadBalancer, Device, Node, session from libra.api.model.lbaas import LoadBalancer, Device, Node, get_session
from libra.api.model.lbaas import loadbalancers_devices, Limits from libra.api.model.lbaas import loadbalancers_devices, Limits
from libra.api.model.validators import LBPut, LBPost, LBResp, LBVipResp from libra.api.model.validators import LBPut, LBPost, LBResp, LBVipResp
from libra.api.model.validators import LBRespNode from libra.api.model.validators import LBRespNode
@ -58,7 +58,7 @@ class LoadBalancersController(RestController):
""" """
tenant_id = get_limited_to_project(request.headers) tenant_id = get_limited_to_project(request.headers)
session = get_session()
# if we don't have an id then we want a list of them own by this tenent # if we don't have an id then we want a list of them own by this tenent
if not load_balancer_id: if not load_balancer_id:
lbs = session.query( lbs = session.query(
@ -177,7 +177,7 @@ class LoadBalancersController(RestController):
raise ClientSideError( raise ClientSideError(
'IP Address {0} not valid'.format(node.address) 'IP Address {0} not valid'.format(node.address)
) )
session = get_session()
lblimit = session.query(Limits.value).\ lblimit = session.query(Limits.value).\
filter(Limits.name == 'maxLoadBalancers').scalar() filter(Limits.name == 'maxLoadBalancers').scalar()
nodelimit = session.query(Limits.value).\ nodelimit = session.query(Limits.value).\
@ -189,16 +189,19 @@ class LoadBalancersController(RestController):
filter(LoadBalancer.status != 'DELETED').count() filter(LoadBalancer.status != 'DELETED').count()
if len(body.name) > namelimit: if len(body.name) > namelimit:
session.rollback()
raise ClientSideError( raise ClientSideError(
'Length of Load Balancer name too long' 'Length of Load Balancer name too long'
) )
# TODO: this should probably be a 413, not sure how to do that yet # TODO: this should probably be a 413, not sure how to do that yet
if count >= lblimit: if count >= lblimit:
session.rollback()
raise OverLimit( raise OverLimit(
'Account has hit limit of {0} Load Balancers'. 'Account has hit limit of {0} Load Balancers'.
format(lblimit) format(lblimit)
) )
if len(body.nodes) > nodelimit: if len(body.nodes) > nodelimit:
session.rollback()
raise OverLimit( raise OverLimit(
'Too many backend nodes supplied (limit is {0}'. 'Too many backend nodes supplied (limit is {0}'.
format(nodelimit) format(nodelimit)
@ -230,6 +233,7 @@ class LoadBalancersController(RestController):
filter(Device.id == virtual_id).\ filter(Device.id == virtual_id).\
first() first()
if old_lb is None: if old_lb is None:
session.rollback()
raise InvalidInput( raise InvalidInput(
'virtualIps', virtual_id, 'Invalid virtual IP provided' 'virtualIps', virtual_id, 'Invalid virtual IP provided'
) )
@ -243,6 +247,7 @@ class LoadBalancersController(RestController):
filter(LoadBalancer.protocol == 'HTTP').\ filter(LoadBalancer.protocol == 'HTTP').\
count() count()
if old_count: if old_count:
session.rollback()
# Error here, can have only one HTTP # Error here, can have only one HTTP
raise ClientSideError( raise ClientSideError(
'Only one HTTP load balancer allowed per device' 'Only one HTTP load balancer allowed per device'
@ -256,12 +261,14 @@ class LoadBalancersController(RestController):
filter(LoadBalancer.protocol == 'TCP').\ filter(LoadBalancer.protocol == 'TCP').\
count() count()
if old_count: if old_count:
session.rollback()
# Error here, can have only one TCP # Error here, can have only one TCP
raise ClientSideError( raise ClientSideError(
'Only one TCP load balancer allowed per device' 'Only one TCP load balancer allowed per device'
) )
if device is None: if device is None:
session.rollback()
raise RuntimeError('No devices available') raise RuntimeError('No devices available')
lb.tenantid = tenant_id lb.tenantid = tenant_id
@ -354,7 +361,7 @@ class LoadBalancersController(RestController):
raise ClientSideError('Load Balancer ID is required') raise ClientSideError('Load Balancer ID is required')
tenant_id = get_limited_to_project(request.headers) tenant_id = get_limited_to_project(request.headers)
session = get_session()
# grab the lb # grab the lb
lb = session.query(LoadBalancer).\ lb = session.query(LoadBalancer).\
filter(LoadBalancer.id == self.lbid).\ filter(LoadBalancer.id == self.lbid).\
@ -362,12 +369,14 @@ class LoadBalancersController(RestController):
filter(LoadBalancer.status != 'DELETED').first() filter(LoadBalancer.status != 'DELETED').first()
if lb is None: if lb is None:
session.rollback()
raise ClientSideError('Load Balancer ID is not valid') raise ClientSideError('Load Balancer ID is not valid')
if body.name != Unset: if body.name != Unset:
namelimit = session.query(Limits.value).\ namelimit = session.query(Limits.value).\
filter(Limits.name == 'maxLoadBalancerNameLength').scalar() filter(Limits.name == 'maxLoadBalancerNameLength').scalar()
if len(body.name) > namelimit: if len(body.name) > namelimit:
session.rollback()
raise ClientSideError( raise ClientSideError(
'Length of Load Balancer name too long' 'Length of Load Balancer name too long'
) )
@ -405,12 +414,14 @@ class LoadBalancersController(RestController):
""" """
tenant_id = get_limited_to_project(request.headers) tenant_id = get_limited_to_project(request.headers)
# grab the lb # grab the lb
session = get_session()
lb = session.query(LoadBalancer).\ lb = session.query(LoadBalancer).\
filter(LoadBalancer.id == load_balancer_id).\ filter(LoadBalancer.id == load_balancer_id).\
filter(LoadBalancer.tenantid == tenant_id).\ filter(LoadBalancer.tenantid == tenant_id).\
filter(LoadBalancer.status != 'DELETED').first() filter(LoadBalancer.status != 'DELETED').first()
if lb is None: if lb is None:
session.rollback()
response.status = 400 response.status = 400
return dict( return dict(
faultcode="Client", faultcode="Client",
@ -435,6 +446,7 @@ class LoadBalancersController(RestController):
response.status = 202 response.status = 202
return '' return ''
except: except:
session.rollback()
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
logger.exception('Error communicating with load balancer pool') logger.exception('Error communicating with load balancer pool')
response.status = 500 response.status = 500

View File

@ -19,7 +19,7 @@ from pecan import conf
import wsmeext.pecan as wsme_pecan import wsmeext.pecan as wsme_pecan
from wsme.exc import ClientSideError from wsme.exc import ClientSideError
from wsme import Unset from wsme import Unset
from libra.api.model.lbaas import LoadBalancer, Device, session from libra.api.model.lbaas import LoadBalancer, Device, get_session
from libra.api.acl import get_limited_to_project from libra.api.acl import get_limited_to_project
from libra.api.model.validators import LBLogsPost from libra.api.model.validators import LBLogsPost
from libra.api.library.gearman_client import submit_job from libra.api.library.gearman_client import submit_job
@ -35,13 +35,14 @@ class LogsController(RestController):
raise ClientSideError('Load Balancer ID has not been supplied') raise ClientSideError('Load Balancer ID has not been supplied')
tenant_id = get_limited_to_project(request.headers) tenant_id = get_limited_to_project(request.headers)
session = get_session()
load_balancer = session.query(LoadBalancer).\ load_balancer = session.query(LoadBalancer).\
filter(LoadBalancer.tenantid == tenant_id).\ filter(LoadBalancer.tenantid == tenant_id).\
filter(LoadBalancer.id == self.lbid).\ filter(LoadBalancer.id == self.lbid).\
filter(LoadBalancer.status != 'DELETED').\ filter(LoadBalancer.status != 'DELETED').\
first() first()
if load_balancer is None: if load_balancer is None:
session.rollback()
raise ClientSideError('Load Balancer not found') raise ClientSideError('Load Balancer not found')
load_balancer.status = 'PENDING_UPDATE' load_balancer.status = 'PENDING_UPDATE'

View File

@ -20,7 +20,8 @@ import wsmeext.pecan as wsme_pecan
from wsme.exc import ClientSideError from wsme.exc import ClientSideError
from wsme import Unset from wsme import Unset
#default response objects #default response objects
from libra.api.model.lbaas import LoadBalancer, Node, session, Limits, Device 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.acl import get_limited_to_project
from libra.api.model.validators import LBNodeResp, LBNodePost, NodeResp from libra.api.model.validators import LBNodeResp, LBNodePost, NodeResp
from libra.api.model.validators import LBNodePut from libra.api.model.validators import LBNodePut
@ -56,7 +57,7 @@ class NodesController(RestController):
faultcode='Client', faultcode='Client',
faultstring='Load Balancer ID not supplied' faultstring='Load Balancer ID not supplied'
) )
session = get_session()
if not node_id: if not node_id:
nodes = session.query( nodes = session.query(
Node.id, Node.address, Node.port, Node.status, Node.enabled Node.id, Node.address, Node.port, Node.status, Node.enabled
@ -132,13 +133,14 @@ class NodesController(RestController):
raise ClientSideError( raise ClientSideError(
'IP Address {0} not valid'.format(node.address) 'IP Address {0} not valid'.format(node.address)
) )
session = get_session()
load_balancer = session.query(LoadBalancer).\ load_balancer = session.query(LoadBalancer).\
filter(LoadBalancer.tenantid == tenant_id).\ filter(LoadBalancer.tenantid == tenant_id).\
filter(LoadBalancer.id == self.lbid).\ filter(LoadBalancer.id == self.lbid).\
filter(LoadBalancer.status != 'DELETED').\ filter(LoadBalancer.status != 'DELETED').\
first() first()
if load_balancer is None: if load_balancer is None:
session.rollback()
raise ClientSideError('Load Balancer not found') raise ClientSideError('Load Balancer not found')
load_balancer.status = 'PENDING_UPDATE' load_balancer.status = 'PENDING_UPDATE'
@ -149,6 +151,7 @@ class NodesController(RestController):
filter(Node.lbid == self.lbid).count() filter(Node.lbid == self.lbid).count()
if (nodecount + len(body.nodes)) > nodelimit: if (nodecount + len(body.nodes)) > nodelimit:
session.rollback()
raise OverLimit( raise OverLimit(
'Command would exceed Load Balancer node limit' 'Command would exceed Load Balancer node limit'
) )
@ -195,6 +198,7 @@ class NodesController(RestController):
raise ClientSideError('Node ID has not been supplied') raise ClientSideError('Node ID has not been supplied')
tenant_id = get_limited_to_project(request.headers) tenant_id = get_limited_to_project(request.headers)
session = get_session()
# grab the lb # grab the lb
lb = session.query(LoadBalancer).\ lb = session.query(LoadBalancer).\
filter(LoadBalancer.id == self.lbid).\ filter(LoadBalancer.id == self.lbid).\
@ -202,6 +206,7 @@ class NodesController(RestController):
filter(LoadBalancer.status != 'DELETED').first() filter(LoadBalancer.status != 'DELETED').first()
if lb is None: if lb is None:
session.rollback()
raise ClientSideError('Load Balancer ID is not valid') raise ClientSideError('Load Balancer ID is not valid')
node = session.query(Node).\ node = session.query(Node).\
@ -209,6 +214,7 @@ class NodesController(RestController):
filter(Node.id == self.nodeid).first() filter(Node.id == self.nodeid).first()
if node is None: if node is None:
session.rollback()
raise ClientSideError('Node ID is not valid') raise ClientSideError('Node ID is not valid')
if body.condition != Unset: if body.condition != Unset:
@ -250,12 +256,14 @@ class NodesController(RestController):
) )
tenant_id = get_limited_to_project(request.headers) tenant_id = get_limited_to_project(request.headers)
session = get_session()
load_balancer = session.query(LoadBalancer).\ load_balancer = session.query(LoadBalancer).\
filter(LoadBalancer.tenantid == tenant_id).\ filter(LoadBalancer.tenantid == tenant_id).\
filter(LoadBalancer.id == self.lbid).\ filter(LoadBalancer.id == self.lbid).\
filter(LoadBalancer.device != 'DELETED').\ filter(LoadBalancer.device != 'DELETED').\
first() first()
if load_balancer is None: if load_balancer is None:
session.rollback()
response.status = 400 response.status = 400
return dict( return dict(
faultcode="Client", faultcode="Client",
@ -266,16 +274,18 @@ class NodesController(RestController):
filter(Node.lbid == self.lbid).count() filter(Node.lbid == self.lbid).count()
# Can't delete the last LB # Can't delete the last LB
if nodecount <= 1: if nodecount <= 1:
session.rollback()
response.status = 400 response.status = 400
return dict( return dict(
faultcode="Client", faultcode="Client",
faultstring="Load Balancer not found" faultstring="Cannot delete the last node in a load balancer"
) )
node = session.query(Node).\ node = session.query(Node).\
filter(Node.lbid == self.lbid).\ filter(Node.lbid == self.lbid).\
filter(Node.id == node_id).\ filter(Node.id == node_id).\
first() first()
if not node: if not node:
session.rollback()
response.status = 400 response.status = 400
return dict( return dict(
faultcode="Client", faultcode="Client",

View File

@ -15,7 +15,7 @@
from pecan import response, expose, request from pecan import response, expose, request
from pecan.rest import RestController from pecan.rest import RestController
from libra.api.model.lbaas import LoadBalancer, Device, session from libra.api.model.lbaas import LoadBalancer, Device, get_session
from libra.api.acl import get_limited_to_project from libra.api.acl import get_limited_to_project
@ -41,6 +41,7 @@ class VipsController(RestController):
faultcode="Client", faultcode="Client",
faultstring="Load Balancer ID not provided" faultstring="Load Balancer ID not provided"
) )
session = get_session()
device = session.query( device = session.query(
Device.id, Device.floatingIpAddr Device.id, Device.floatingIpAddr
).join(LoadBalancer.devices).\ ).join(LoadBalancer.devices).\
@ -48,6 +49,7 @@ class VipsController(RestController):
filter(LoadBalancer.tenantid == tenant_id).first() filter(LoadBalancer.tenantid == tenant_id).first()
if not device: if not device:
session.rollback()
response.status = 400 response.status = 400
return dict( return dict(
faultcode="Client", faultcode="Client",
@ -61,5 +63,5 @@ class VipsController(RestController):
"ipVersion": "IPV4" "ipVersion": "IPV4"
}] }]
} }
session.rollback()
return resp return resp

View File

@ -16,7 +16,7 @@ import eventlet
eventlet.monkey_patch() eventlet.monkey_patch()
import logging import logging
from libra.common.json_gearman import JSONGearmanClient from libra.common.json_gearman import JSONGearmanClient
from libra.api.model.lbaas import LoadBalancer, session, Device from libra.api.model.lbaas import LoadBalancer, get_session, Device
from pecan import conf from pecan import conf
@ -56,6 +56,7 @@ class GearmanClientThread(object):
self.gearman_client = JSONGearmanClient(conf.gearman.server) self.gearman_client = JSONGearmanClient(conf.gearman.server)
def send_delete(self, data): def send_delete(self, data):
session = get_session()
count = session.query( count = session.query(
LoadBalancer LoadBalancer
).join(LoadBalancer.devices).\ ).join(LoadBalancer.devices).\
@ -100,7 +101,7 @@ class GearmanClientThread(object):
filter(LoadBalancer.id == self.lbid).\ filter(LoadBalancer.id == self.lbid).\
first() first()
if not status: if not status:
self._set_error(data, response) self._set_error(data, response, session)
else: else:
lb.status = 'DELETED' lb.status = 'DELETED'
if count == 0: if count == 0:
@ -111,7 +112,7 @@ class GearmanClientThread(object):
device.status = 'OFFLINE' device.status = 'OFFLINE'
session.commit() session.commit()
def _set_error(self, device_id, errmsg): def _set_error(self, device_id, errmsg, session):
lbs = session.query( lbs = session.query(
LoadBalancer LoadBalancer
).join(LoadBalancer.nodes).\ ).join(LoadBalancer.nodes).\
@ -128,6 +129,7 @@ class GearmanClientThread(object):
lb.errmsg = errmsg lb.errmsg = errmsg
def send_archive(self, data): def send_archive(self, data):
session = get_session()
lb = session.query(LoadBalancer).\ lb = session.query(LoadBalancer).\
filter(LoadBalancer.id == self.lbid).\ filter(LoadBalancer.id == self.lbid).\
first() first()
@ -155,6 +157,7 @@ class GearmanClientThread(object):
session.commit() session.commit()
def send_update(self, data): def send_update(self, data):
session = get_session()
lbs = session.query( lbs = session.query(
LoadBalancer LoadBalancer
).join(LoadBalancer.nodes).\ ).join(LoadBalancer.nodes).\
@ -190,7 +193,7 @@ class GearmanClientThread(object):
filter(LoadBalancer.id == self.lbid).\ filter(LoadBalancer.id == self.lbid).\
first() first()
if not status: if not status:
self._set_error(data, response) self._set_error(data, response, session)
else: else:
lb.status = 'ACTIVE' lb.status = 'ACTIVE'
session.commit() session.commit()

View File

@ -113,5 +113,6 @@ class Node(DeclarativeBase):
weight = Column(u'weight', INTEGER(), nullable=False) weight = Column(u'weight', INTEGER(), nullable=False)
"""session""" def get_session():
session = sessionmaker(bind=engine)() session = sessionmaker(bind=engine)()
return session