From df060ccd19986c7346258718b7c76b0a4e16f7ac Mon Sep 17 00:00:00 2001 From: Andrew Hutchings Date: Mon, 10 Jun 2013 14:10:30 +0100 Subject: [PATCH] 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 --- etc/sample_libra.cfg | 2 ++ libra/admin_api/app.py | 26 +++++++++++++++++++++++-- libra/admin_api/config.py | 23 ---------------------- libra/admin_api/controllers/devices.py | 4 ++-- libra/admin_api/controllers/usage.py | 2 +- libra/api/app.py | 4 ++++ libra/api/config.py | 23 ---------------------- libra/api/controllers/limits.py | 4 +++- libra/api/controllers/load_balancers.py | 20 +++++++++++++++---- libra/api/controllers/logs.py | 5 +++-- libra/api/controllers/nodes.py | 18 +++++++++++++---- libra/api/controllers/virtualips.py | 6 ++++-- libra/api/library/gearman_client.py | 11 +++++++---- libra/api/model/lbaas.py | 5 +++-- 14 files changed, 83 insertions(+), 70 deletions(-) diff --git a/etc/sample_libra.cfg b/etc/sample_libra.cfg index cddc1d72..4b890c6d 100644 --- a/etc/sample_libra.cfg +++ b/etc/sample_libra.cfg @@ -76,6 +76,8 @@ poll_timeout_retry = 30 db_user=root db_pass=passwd db_schema=lbaas +ssl_certfile=certfile.crt +ssl_keyfile=keyfile.key [api] host=0.0.0.0 diff --git a/libra/admin_api/app.py b/libra/admin_api/app.py index c43af4a3..84d24364 100644 --- a/libra/admin_api/app.py +++ b/libra/admin_api/app.py @@ -46,6 +46,10 @@ def setup_app(pecan_config, args): 'host': args.db_host, 'schema': args.db_schema } + if args.debug: + config['wsme'] = {'debug': True} + config['app']['debug'] = True + pecan.configuration.set_config(config, overwrite=True) app = pecan.make_app( @@ -93,11 +97,20 @@ def main(): options.parser.add_argument( '--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() 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 @@ -131,6 +144,15 @@ def main(): logger.info('Starting on {0}:{1}'.format(args.host, args.port)) api = setup_app(pc, args) 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 diff --git a/libra/admin_api/config.py b/libra/admin_api/config.py index 751b7f64..9ad91482 100644 --- a/libra/admin_api/config.py +++ b/libra/admin_api/config.py @@ -19,31 +19,8 @@ app = { 'modules': ['libra.admin_api'], 'static_root': '%(confdir)s/public', 'template_path': '%(confdir)s/admin_api/templates', - 'debug': True, 'errors': { 404: '/notfound', '__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 diff --git a/libra/admin_api/controllers/devices.py b/libra/admin_api/controllers/devices.py index 72309290..20c9cc1c 100644 --- a/libra/admin_api/controllers/devices.py +++ b/libra/admin_api/controllers/devices.py @@ -21,8 +21,8 @@ import wsmeext.pecan as wsme_pecan from wsme.exc import ClientSideError from usage import UsageController from libra.admin_api.model.validators import DeviceResp, DevicePost, DevicePut -from libra.api.model.lbaas import LoadBalancer, Device, session -from libra.api.model.lbaas import loadbalancers_devices +from libra.admin_api.model.lbaas import LoadBalancer, Device, session +from libra.admin_api.model.lbaas import loadbalancers_devices class DevicesController(RestController): diff --git a/libra/admin_api/controllers/usage.py b/libra/admin_api/controllers/usage.py index 5172eacd..437bcecd 100644 --- a/libra/admin_api/controllers/usage.py +++ b/libra/admin_api/controllers/usage.py @@ -14,7 +14,7 @@ # under the License. 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 pecan.rest import RestController diff --git a/libra/api/app.py b/libra/api/app.py index cc89ef11..6e05ed45 100644 --- a/libra/api/app.py +++ b/libra/api/app.py @@ -59,6 +59,10 @@ def setup_app(pecan_config, args): config['gearman'] = { 'server': args.gearman } + if args.debug: + config['wsme'] = {'debug': True} + config['app']['debug'] = True + pecan.configuration.set_config(config, overwrite=True) app = pecan.make_app( diff --git a/libra/api/config.py b/libra/api/config.py index 7fe69270..42779a48 100644 --- a/libra/api/config.py +++ b/libra/api/config.py @@ -19,31 +19,8 @@ app = { 'modules': ['libra.api'], 'static_root': '%(confdir)s/public', 'template_path': '%(confdir)s/api/templates', - 'debug': True, 'errors': { 404: '/notfound', '__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 diff --git a/libra/api/controllers/limits.py b/libra/api/controllers/limits.py index 25969bd2..8290519f 100644 --- a/libra/api/controllers/limits.py +++ b/libra/api/controllers/limits.py @@ -15,16 +15,18 @@ from pecan import expose 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): @expose('json') def get(self): resp = {} + session = get_session() limits = session.query(Limits).all() for limit in limits: resp[limit.name] = limit.value resp = {"limits": {"absolute": {"values": resp}}} + session.rollback() return resp diff --git a/libra/api/controllers/load_balancers.py b/libra/api/controllers/load_balancers.py index ab93e88b..6da033d1 100644 --- a/libra/api/controllers/load_balancers.py +++ b/libra/api/controllers/load_balancers.py @@ -25,7 +25,7 @@ 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 LoadBalancer, Device, Node, get_session 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 LBRespNode @@ -58,7 +58,7 @@ class LoadBalancersController(RestController): """ 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 not load_balancer_id: lbs = session.query( @@ -177,7 +177,7 @@ class LoadBalancersController(RestController): raise ClientSideError( 'IP Address {0} not valid'.format(node.address) ) - + session = get_session() lblimit = session.query(Limits.value).\ filter(Limits.name == 'maxLoadBalancers').scalar() nodelimit = session.query(Limits.value).\ @@ -189,16 +189,19 @@ class LoadBalancersController(RestController): filter(LoadBalancer.status != 'DELETED').count() if len(body.name) > namelimit: + session.rollback() raise ClientSideError( 'Length of Load Balancer name too long' ) # TODO: this should probably be a 413, not sure how to do that yet if count >= lblimit: + session.rollback() raise OverLimit( 'Account has hit limit of {0} Load Balancers'. format(lblimit) ) if len(body.nodes) > nodelimit: + session.rollback() raise OverLimit( 'Too many backend nodes supplied (limit is {0}'. format(nodelimit) @@ -230,6 +233,7 @@ class LoadBalancersController(RestController): filter(Device.id == virtual_id).\ first() if old_lb is None: + session.rollback() raise InvalidInput( 'virtualIps', virtual_id, 'Invalid virtual IP provided' ) @@ -243,6 +247,7 @@ class LoadBalancersController(RestController): filter(LoadBalancer.protocol == 'HTTP').\ count() if old_count: + session.rollback() # Error here, can have only one HTTP raise ClientSideError( 'Only one HTTP load balancer allowed per device' @@ -256,12 +261,14 @@ class LoadBalancersController(RestController): filter(LoadBalancer.protocol == 'TCP').\ count() if old_count: + session.rollback() # Error here, can have only one TCP raise ClientSideError( 'Only one TCP load balancer allowed per device' ) if device is None: + session.rollback() raise RuntimeError('No devices available') lb.tenantid = tenant_id @@ -354,7 +361,7 @@ class LoadBalancersController(RestController): raise ClientSideError('Load Balancer ID is required') tenant_id = get_limited_to_project(request.headers) - + session = get_session() # grab the lb lb = session.query(LoadBalancer).\ filter(LoadBalancer.id == self.lbid).\ @@ -362,12 +369,14 @@ class LoadBalancersController(RestController): filter(LoadBalancer.status != 'DELETED').first() if lb is None: + session.rollback() raise ClientSideError('Load Balancer ID is not valid') if body.name != Unset: namelimit = session.query(Limits.value).\ filter(Limits.name == 'maxLoadBalancerNameLength').scalar() if len(body.name) > namelimit: + session.rollback() raise ClientSideError( 'Length of Load Balancer name too long' ) @@ -405,12 +414,14 @@ class LoadBalancersController(RestController): """ tenant_id = get_limited_to_project(request.headers) # grab the lb + session = get_session() lb = session.query(LoadBalancer).\ filter(LoadBalancer.id == load_balancer_id).\ filter(LoadBalancer.tenantid == tenant_id).\ filter(LoadBalancer.status != 'DELETED').first() if lb is None: + session.rollback() response.status = 400 return dict( faultcode="Client", @@ -435,6 +446,7 @@ class LoadBalancersController(RestController): response.status = 202 return '' except: + session.rollback() logger = logging.getLogger(__name__) logger.exception('Error communicating with load balancer pool') response.status = 500 diff --git a/libra/api/controllers/logs.py b/libra/api/controllers/logs.py index f0a02a96..20f5a109 100644 --- a/libra/api/controllers/logs.py +++ b/libra/api/controllers/logs.py @@ -19,7 +19,7 @@ from pecan import conf import wsmeext.pecan as wsme_pecan from wsme.exc import ClientSideError 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.model.validators import LBLogsPost 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') 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.status != 'DELETED').\ first() if load_balancer is None: + session.rollback() raise ClientSideError('Load Balancer not found') load_balancer.status = 'PENDING_UPDATE' diff --git a/libra/api/controllers/nodes.py b/libra/api/controllers/nodes.py index 2a940af5..9646be13 100644 --- a/libra/api/controllers/nodes.py +++ b/libra/api/controllers/nodes.py @@ -20,7 +20,8 @@ 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, 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.model.validators import LBNodeResp, LBNodePost, NodeResp from libra.api.model.validators import LBNodePut @@ -56,7 +57,7 @@ class NodesController(RestController): 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 @@ -132,13 +133,14 @@ class NodesController(RestController): 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' @@ -149,6 +151,7 @@ class NodesController(RestController): filter(Node.lbid == self.lbid).count() if (nodecount + len(body.nodes)) > nodelimit: + session.rollback() raise OverLimit( 'Command would exceed Load Balancer node limit' ) @@ -195,6 +198,7 @@ class NodesController(RestController): 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).\ @@ -202,6 +206,7 @@ class NodesController(RestController): filter(LoadBalancer.status != 'DELETED').first() if lb is None: + session.rollback() raise ClientSideError('Load Balancer ID is not valid') node = session.query(Node).\ @@ -209,6 +214,7 @@ class NodesController(RestController): filter(Node.id == self.nodeid).first() if node is None: + session.rollback() raise ClientSideError('Node ID is not valid') if body.condition != Unset: @@ -250,12 +256,14 @@ class NodesController(RestController): ) 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", @@ -266,16 +274,18 @@ class NodesController(RestController): 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="Load Balancer not found" + 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", diff --git a/libra/api/controllers/virtualips.py b/libra/api/controllers/virtualips.py index 1baac6c0..636527b1 100644 --- a/libra/api/controllers/virtualips.py +++ b/libra/api/controllers/virtualips.py @@ -15,7 +15,7 @@ from pecan import response, expose, request 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 @@ -41,6 +41,7 @@ class VipsController(RestController): faultcode="Client", faultstring="Load Balancer ID not provided" ) + session = get_session() device = session.query( Device.id, Device.floatingIpAddr ).join(LoadBalancer.devices).\ @@ -48,6 +49,7 @@ class VipsController(RestController): filter(LoadBalancer.tenantid == tenant_id).first() if not device: + session.rollback() response.status = 400 return dict( faultcode="Client", @@ -61,5 +63,5 @@ class VipsController(RestController): "ipVersion": "IPV4" }] } - + session.rollback() return resp diff --git a/libra/api/library/gearman_client.py b/libra/api/library/gearman_client.py index 8e35cd33..660d546f 100644 --- a/libra/api/library/gearman_client.py +++ b/libra/api/library/gearman_client.py @@ -16,7 +16,7 @@ import eventlet eventlet.monkey_patch() import logging 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 @@ -56,6 +56,7 @@ class GearmanClientThread(object): self.gearman_client = JSONGearmanClient(conf.gearman.server) def send_delete(self, data): + session = get_session() count = session.query( LoadBalancer ).join(LoadBalancer.devices).\ @@ -100,7 +101,7 @@ class GearmanClientThread(object): filter(LoadBalancer.id == self.lbid).\ first() if not status: - self._set_error(data, response) + self._set_error(data, response, session) else: lb.status = 'DELETED' if count == 0: @@ -111,7 +112,7 @@ class GearmanClientThread(object): device.status = 'OFFLINE' session.commit() - def _set_error(self, device_id, errmsg): + def _set_error(self, device_id, errmsg, session): lbs = session.query( LoadBalancer ).join(LoadBalancer.nodes).\ @@ -128,6 +129,7 @@ class GearmanClientThread(object): lb.errmsg = errmsg def send_archive(self, data): + session = get_session() lb = session.query(LoadBalancer).\ filter(LoadBalancer.id == self.lbid).\ first() @@ -155,6 +157,7 @@ class GearmanClientThread(object): session.commit() def send_update(self, data): + session = get_session() lbs = session.query( LoadBalancer ).join(LoadBalancer.nodes).\ @@ -190,7 +193,7 @@ class GearmanClientThread(object): filter(LoadBalancer.id == self.lbid).\ first() if not status: - self._set_error(data, response) + self._set_error(data, response, session) else: lb.status = 'ACTIVE' session.commit() diff --git a/libra/api/model/lbaas.py b/libra/api/model/lbaas.py index 8172b5f8..7bae6cb1 100644 --- a/libra/api/model/lbaas.py +++ b/libra/api/model/lbaas.py @@ -113,5 +113,6 @@ class Node(DeclarativeBase): weight = Column(u'weight', INTEGER(), nullable=False) -"""session""" -session = sessionmaker(bind=engine)() +def get_session(): + session = sessionmaker(bind=engine)() + return session