[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:
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -46,6 +46,10 @@ class Responses(object):
|
|||||||
{
|
{
|
||||||
'name': 'TCP',
|
'name': 'TCP',
|
||||||
'port': '443'
|
'port': '443'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'GALERA',
|
||||||
|
'port': '3306'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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':
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user