[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(
'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)

View File

@@ -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

View File

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

View File

@@ -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):

View File

@@ -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':

View File

@@ -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):

View File

@@ -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;