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