diff --git a/libra/api/controllers/load_balancers.py b/libra/api/controllers/load_balancers.py index 5c1a81a9..caa50913 100644 --- a/libra/api/controllers/load_balancers.py +++ b/libra/api/controllers/load_balancers.py @@ -180,6 +180,16 @@ class LoadBalancersController(RestController): raise ClientSideError( 'At least one backend node needs to be supplied' ) + + # When the load balancer is used for Galera, we need to do some + # sanity checking of the nodes to make sure 1 and only 1 node is + # defined as the primary node. + if body.protocol and body.protocol.lower() == 'galera': + is_galera = True + else: + is_galera = False + num_galera_primary_nodes = 0 + for node in body.nodes: if node.address == Unset: raise ClientSideError( @@ -194,6 +204,7 @@ class LoadBalancersController(RestController): 'Node {0} port number {1} is invalid' .format(node.address, node.port) ) + try: node.address = ipfilter(node.address, conf.ip_filters) except IPOutOfRange: @@ -205,6 +216,19 @@ class LoadBalancersController(RestController): raise ClientSideError( 'IP Address {0} not valid'.format(node.address) ) + + is_backup = False + if node.backup != Unset and node.backup == 'TRUE': + is_backup = True + if is_galera and not is_backup: + num_galera_primary_nodes += 1 + + # Galera sanity checks + if is_galera and num_galera_primary_nodes != 1: + raise ClientSideError( + 'Galera load balancer must have exactly one primary node' + ) + with db_session() as session: lblimit = session.query(Limits.value).\ filter(Limits.name == 'maxLoadBalancers').scalar() @@ -241,8 +265,13 @@ class LoadBalancersController(RestController): lb = LoadBalancer() lb.tenantid = tenant_id lb.name = body.name - if body.protocol and body.protocol.lower() == 'tcp': - lb.protocol = 'TCP' + if body.protocol: + if body.protocol.lower() in ('tcp', 'http', 'galera'): + lb.protocol = body.protocol.upper() + else: + raise ClientSideError( + 'Invalid protocol %s' % body.protocol + ) else: lb.protocol = 'HTTP' @@ -255,8 +284,10 @@ class LoadBalancersController(RestController): else: if lb.protocol == 'HTTP': lb.port = 80 - else: + elif lb.protocol == 'TCP': lb.port = 443 + elif lb.protocol == 'GALERA': + lb.port = 3306 lb.status = 'BUILD' lb.created = None @@ -351,9 +382,16 @@ class LoadBalancersController(RestController): else: enabled = 1 node_status = 'ONLINE' + + if node.backup == 'TRUE': + backup = 1 + else: + backup = 0 + out_node = Node( lbid=lb.id, port=node.port, address=node.address, - enabled=enabled, status=node_status, weight=1 + enabled=enabled, status=node_status, weight=1, + backup=backup ) session.add(out_node) diff --git a/libra/api/controllers/nodes.py b/libra/api/controllers/nodes.py index 66078da0..63079fb8 100644 --- a/libra/api/controllers/nodes.py +++ b/libra/api/controllers/nodes.py @@ -146,6 +146,7 @@ class NodesController(RestController): raise ClientSideError( 'IP Address {0} not valid'.format(node.address) ) + with db_session() as session: load_balancer = session.query(LoadBalancer).\ filter(LoadBalancer.tenantid == tenant_id).\ @@ -157,20 +158,36 @@ class NodesController(RestController): raise NotFound('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 = [] + + is_galera = False + if load_balancer.protocol.lower() == 'galera': + is_galera = True + for node in body.nodes: + is_backup = False + if node.backup != Unset and node.backup == 'TRUE': + is_backup = True + + # Galera load balancer sanity checking. Only allowed to add + # backup nodes since a primary is presumably already defined. + if is_galera and not is_backup: + raise ClientSideError( + 'Galera load balancer may have only one primary node' + ) if node.condition == 'DISABLED': enabled = 0 node_status = 'OFFLINE' @@ -179,7 +196,8 @@ class NodesController(RestController): node_status = 'ONLINE' new_node = Node( lbid=self.lbid, port=node.port, address=node.address, - enabled=enabled, status=node_status, weight=1 + enabled=enabled, status=node_status, + weight=1, backup=int(is_backup) ) session.add(new_node) session.flush() @@ -194,6 +212,7 @@ class NodesController(RestController): status=new_node.status ) ) + device = session.query( Device.id, Device.name, Device.status ).join(LoadBalancer.devices).\ @@ -214,6 +233,7 @@ class NodesController(RestController): @wsme_pecan.wsexpose(None, body=LBNodePut, status_code=202) def put(self, body=None): + """ Update a node condition: ENABLED or DISABLED """ if not self.lbid: raise ClientSideError('Load Balancer ID has not been supplied') if not self.nodeid: @@ -301,7 +321,9 @@ class NodesController(RestController): if load_balancer is None: session.rollback() raise NotFound("Load Balancer not found") + load_balancer.status = 'PENDING_UPDATE' + nodecount = session.query(Node).\ filter(Node.lbid == self.lbid).\ filter(Node.enabled == 1).count() @@ -311,6 +333,7 @@ class NodesController(RestController): raise ClientSideError( "Cannot delete the last enabled node in a load balancer" ) + node = session.query(Node).\ filter(Node.lbid == self.lbid).\ filter(Node.id == node_id).\ @@ -320,6 +343,14 @@ class NodesController(RestController): raise NotFound( "Node not found in supplied Load Balancer" ) + + # May not delete the primary node of a Galera LB + if load_balancer.protocol.lower() == 'galera' and node.backup == 0: + session.rollback() + raise ClientSideError( + "Cannot delete the primary node in a Galera load balancer" + ) + session.delete(node) device = session.query( Device.id, Device.name diff --git a/libra/api/model/responses.py b/libra/api/model/responses.py index 03c43eb5..a77fbaeb 100644 --- a/libra/api/model/responses.py +++ b/libra/api/model/responses.py @@ -46,6 +46,10 @@ class Responses(object): { 'name': 'TCP', 'port': '443' + }, + { + 'name': 'GALERA', + 'port': '3306' } ] } diff --git a/libra/api/model/validators.py b/libra/api/model/validators.py index bac4904e..4a346ece 100644 --- a/libra/api/model/validators.py +++ b/libra/api/model/validators.py @@ -22,6 +22,7 @@ class LBNode(Base): port = wsattr(int, mandatory=True) address = wsattr(wtypes.text, mandatory=True) condition = Enum(wtypes.text, 'ENABLED', 'DISABLED') + backup = Enum(wtypes.text, 'TRUE', 'FALSE') class LBRespNode(Base): diff --git a/libra/common/api/gearman_client.py b/libra/common/api/gearman_client.py index 6050cc01..cee95180 100644 --- a/libra/common/api/gearman_client.py +++ b/libra/common/api/gearman_client.py @@ -272,11 +272,15 @@ class GearmanClientThread(object): if not node.enabled: continue condition = 'ENABLED' + backup = 'FALSE' + if node.backup != 0: + backup = 'TRUE' node_data = { 'id': node.id, 'port': node.port, 'address': node.address, 'weight': node.weight, - 'condition': condition + 'condition': condition, 'backup': backup } + lb_data['nodes'].append(node_data) # Track if we have a DEGRADED LB if node.status == 'ERROR': diff --git a/libra/common/api/lbaas.py b/libra/common/api/lbaas.py index 419539f3..2b6985af 100644 --- a/libra/common/api/lbaas.py +++ b/libra/common/api/lbaas.py @@ -117,7 +117,7 @@ class Node(DeclarativeBase): __tablename__ = 'nodes' #column definitions address = Column(u'address', VARCHAR(length=128), nullable=False) - enabled = Column(u'enabled', Integer(), nullable=False) + enabled = Column(u'enabled', INTEGER(), nullable=False) id = Column(u'id', BIGINT(), primary_key=True, nullable=False) lbid = Column( u'lbid', BIGINT(), ForeignKey('loadbalancers.id'), nullable=False @@ -125,6 +125,7 @@ class Node(DeclarativeBase): port = Column(u'port', INTEGER(), nullable=False) status = Column(u'status', VARCHAR(length=128), nullable=False) weight = Column(u'weight', INTEGER(), nullable=False) + backup = Column(u'backup', INTEGER(), nullable=False, default=0) class HealthMonitor(DeclarativeBase): diff --git a/libra/common/api/lbaas.sql b/libra/common/api/lbaas.sql index 60881a7c..7c4987d3 100644 --- a/libra/common/api/lbaas.sql +++ b/libra/common/api/lbaas.sql @@ -38,6 +38,7 @@ CREATE TABLE loadbalancers ( weight INT NOT NULL, # Node weight if applicable to algorithm used enabled BOOLEAN NOT NULL, # is node enabled or not status VARCHAR(128) NOT NULL, # status of node 'OFFLINE', 'ONLINE', 'ERROR', this value is reported by the device + backup BOOLEAN NOT NULL DEFAULT FALSE, # true if a backup node PRIMARY KEY (id) # ids are unique accross all Nodes ) DEFAULT CHARSET utf8 DEFAULT COLLATE utf8_general_ci;