[API] Support new GALERA protocol in the API

When a Galera load balancer is requested, we validate the
node definitions in the API rather than rely on the worker
for validation. Make sure that number of primary nodes is
one, no more and no less.

When using POST to add nodes, we check that a primary node
is not being added. We assume a primary is already defined
since the LB cannot exist without a primary.

DELETE is checked to make sure that the primary node is not
being removed.

Set the default port for GALERA protocol to 3306.

Change-Id: I8cf1489e1cae6c790bc05c5573dee959f93d20c9
This commit is contained in:
David Shrewsbury
2013-09-25 21:24:55 +00:00
parent 684cb24611
commit 627d89b5ff
7 changed files with 88 additions and 8 deletions

View File

@@ -180,6 +180,16 @@ class LoadBalancersController(RestController):
raise ClientSideError( raise ClientSideError(
'At least one backend node needs to be supplied' '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: for node in body.nodes:
if node.address == Unset: if node.address == Unset:
raise ClientSideError( raise ClientSideError(
@@ -194,6 +204,7 @@ class LoadBalancersController(RestController):
'Node {0} port number {1} is invalid' 'Node {0} port number {1} is invalid'
.format(node.address, node.port) .format(node.address, node.port)
) )
try: try:
node.address = ipfilter(node.address, conf.ip_filters) node.address = ipfilter(node.address, conf.ip_filters)
except IPOutOfRange: except IPOutOfRange:
@@ -205,6 +216,19 @@ class LoadBalancersController(RestController):
raise ClientSideError( raise ClientSideError(
'IP Address {0} not valid'.format(node.address) '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: with db_session() as session:
lblimit = session.query(Limits.value).\ lblimit = session.query(Limits.value).\
filter(Limits.name == 'maxLoadBalancers').scalar() filter(Limits.name == 'maxLoadBalancers').scalar()
@@ -241,8 +265,13 @@ class LoadBalancersController(RestController):
lb = LoadBalancer() lb = LoadBalancer()
lb.tenantid = tenant_id lb.tenantid = tenant_id
lb.name = body.name lb.name = body.name
if body.protocol and body.protocol.lower() == 'tcp': if body.protocol:
lb.protocol = 'TCP' if body.protocol.lower() in ('tcp', 'http', 'galera'):
lb.protocol = body.protocol.upper()
else:
raise ClientSideError(
'Invalid protocol %s' % body.protocol
)
else: else:
lb.protocol = 'HTTP' lb.protocol = 'HTTP'
@@ -255,8 +284,10 @@ class LoadBalancersController(RestController):
else: else:
if lb.protocol == 'HTTP': if lb.protocol == 'HTTP':
lb.port = 80 lb.port = 80
else: elif lb.protocol == 'TCP':
lb.port = 443 lb.port = 443
elif lb.protocol == 'GALERA':
lb.port = 3306
lb.status = 'BUILD' lb.status = 'BUILD'
lb.created = None lb.created = None
@@ -351,9 +382,16 @@ class LoadBalancersController(RestController):
else: else:
enabled = 1 enabled = 1
node_status = 'ONLINE' node_status = 'ONLINE'
if node.backup == 'TRUE':
backup = 1
else:
backup = 0
out_node = Node( out_node = Node(
lbid=lb.id, port=node.port, address=node.address, 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) session.add(out_node)

View File

@@ -146,6 +146,7 @@ class NodesController(RestController):
raise ClientSideError( raise ClientSideError(
'IP Address {0} not valid'.format(node.address) 'IP Address {0} not valid'.format(node.address)
) )
with db_session() as session: with db_session() as session:
load_balancer = session.query(LoadBalancer).\ load_balancer = session.query(LoadBalancer).\
filter(LoadBalancer.tenantid == tenant_id).\ filter(LoadBalancer.tenantid == tenant_id).\
@@ -157,20 +158,36 @@ class NodesController(RestController):
raise NotFound('Load Balancer not found') raise NotFound('Load Balancer not found')
load_balancer.status = 'PENDING_UPDATE' load_balancer.status = 'PENDING_UPDATE'
# check if we are over limit # check if we are over limit
nodelimit = session.query(Limits.value).\ nodelimit = session.query(Limits.value).\
filter(Limits.name == 'maxNodesPerLoadBalancer').scalar() filter(Limits.name == 'maxNodesPerLoadBalancer').scalar()
nodecount = session.query(Node).\ nodecount = session.query(Node).\
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() session.rollback()
raise OverLimit( raise OverLimit(
'Command would exceed Load Balancer node limit' 'Command would exceed Load Balancer node limit'
) )
return_data = LBNodeResp() return_data = LBNodeResp()
return_data.nodes = [] return_data.nodes = []
is_galera = False
if load_balancer.protocol.lower() == 'galera':
is_galera = True
for node in body.nodes: 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': if node.condition == 'DISABLED':
enabled = 0 enabled = 0
node_status = 'OFFLINE' node_status = 'OFFLINE'
@@ -179,7 +196,8 @@ class NodesController(RestController):
node_status = 'ONLINE' node_status = 'ONLINE'
new_node = Node( new_node = Node(
lbid=self.lbid, port=node.port, address=node.address, 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.add(new_node)
session.flush() session.flush()
@@ -194,6 +212,7 @@ class NodesController(RestController):
status=new_node.status status=new_node.status
) )
) )
device = session.query( device = session.query(
Device.id, Device.name, Device.status Device.id, Device.name, Device.status
).join(LoadBalancer.devices).\ ).join(LoadBalancer.devices).\
@@ -214,6 +233,7 @@ class NodesController(RestController):
@wsme_pecan.wsexpose(None, body=LBNodePut, status_code=202) @wsme_pecan.wsexpose(None, body=LBNodePut, status_code=202)
def put(self, body=None): def put(self, body=None):
""" Update a node condition: ENABLED or DISABLED """
if not self.lbid: if not self.lbid:
raise ClientSideError('Load Balancer ID has not been supplied') raise ClientSideError('Load Balancer ID has not been supplied')
if not self.nodeid: if not self.nodeid:
@@ -301,7 +321,9 @@ class NodesController(RestController):
if load_balancer is None: if load_balancer is None:
session.rollback() session.rollback()
raise NotFound("Load Balancer not found") raise NotFound("Load Balancer not found")
load_balancer.status = 'PENDING_UPDATE' load_balancer.status = 'PENDING_UPDATE'
nodecount = session.query(Node).\ nodecount = session.query(Node).\
filter(Node.lbid == self.lbid).\ filter(Node.lbid == self.lbid).\
filter(Node.enabled == 1).count() filter(Node.enabled == 1).count()
@@ -311,6 +333,7 @@ class NodesController(RestController):
raise ClientSideError( raise ClientSideError(
"Cannot delete the last enabled node in a load balancer" "Cannot delete the last enabled 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).\
@@ -320,6 +343,14 @@ class NodesController(RestController):
raise NotFound( raise NotFound(
"Node not found in supplied Load Balancer" "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) session.delete(node)
device = session.query( device = session.query(
Device.id, Device.name Device.id, Device.name

View File

@@ -46,6 +46,10 @@ class Responses(object):
{ {
'name': 'TCP', 'name': 'TCP',
'port': '443' 'port': '443'
},
{
'name': 'GALERA',
'port': '3306'
} }
] ]
} }

View File

@@ -22,6 +22,7 @@ class LBNode(Base):
port = wsattr(int, mandatory=True) port = wsattr(int, mandatory=True)
address = wsattr(wtypes.text, mandatory=True) address = wsattr(wtypes.text, mandatory=True)
condition = Enum(wtypes.text, 'ENABLED', 'DISABLED') condition = Enum(wtypes.text, 'ENABLED', 'DISABLED')
backup = Enum(wtypes.text, 'TRUE', 'FALSE')
class LBRespNode(Base): class LBRespNode(Base):

View File

@@ -272,11 +272,15 @@ class GearmanClientThread(object):
if not node.enabled: if not node.enabled:
continue continue
condition = 'ENABLED' condition = 'ENABLED'
backup = 'FALSE'
if node.backup != 0:
backup = 'TRUE'
node_data = { node_data = {
'id': node.id, 'port': node.port, 'id': node.id, 'port': node.port,
'address': node.address, 'weight': node.weight, 'address': node.address, 'weight': node.weight,
'condition': condition 'condition': condition, 'backup': backup
} }
lb_data['nodes'].append(node_data) lb_data['nodes'].append(node_data)
# Track if we have a DEGRADED LB # Track if we have a DEGRADED LB
if node.status == 'ERROR': if node.status == 'ERROR':

View File

@@ -117,7 +117,7 @@ class Node(DeclarativeBase):
__tablename__ = 'nodes' __tablename__ = 'nodes'
#column definitions #column definitions
address = Column(u'address', VARCHAR(length=128), nullable=False) 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) id = Column(u'id', BIGINT(), primary_key=True, nullable=False)
lbid = Column( lbid = Column(
u'lbid', BIGINT(), ForeignKey('loadbalancers.id'), nullable=False u'lbid', BIGINT(), ForeignKey('loadbalancers.id'), nullable=False
@@ -125,6 +125,7 @@ class Node(DeclarativeBase):
port = Column(u'port', INTEGER(), nullable=False) port = Column(u'port', INTEGER(), nullable=False)
status = Column(u'status', VARCHAR(length=128), nullable=False) status = Column(u'status', VARCHAR(length=128), nullable=False)
weight = Column(u'weight', INTEGER(), nullable=False) weight = Column(u'weight', INTEGER(), nullable=False)
backup = Column(u'backup', INTEGER(), nullable=False, default=0)
class HealthMonitor(DeclarativeBase): class HealthMonitor(DeclarativeBase):

View File

@@ -38,6 +38,7 @@ CREATE TABLE loadbalancers (
weight INT NOT NULL, # Node weight if applicable to algorithm used weight INT NOT NULL, # Node weight if applicable to algorithm used
enabled BOOLEAN NOT NULL, # is node enabled or not 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 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 PRIMARY KEY (id) # ids are unique accross all Nodes
) DEFAULT CHARSET utf8 DEFAULT COLLATE utf8_general_ci; ) DEFAULT CHARSET utf8 DEFAULT COLLATE utf8_general_ci;