231 lines
6.3 KiB
Python
231 lines
6.3 KiB
Python
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
# not use this file except in compliance with the License. You may obtain
|
|
# a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
# License for the specific language governing permissions and limitations
|
|
# under the License.
|
|
|
|
import datetime
|
|
|
|
from oslo_utils import timeutils
|
|
import six
|
|
|
|
from heat.common import exception
|
|
from heat.common.i18n import _
|
|
from heat.engine import constraints
|
|
from heat.engine import properties
|
|
from heat.engine import resource
|
|
|
|
try:
|
|
from pyrax.exceptions import NotFound # noqa
|
|
PYRAX_INSTALLED = True
|
|
except ImportError:
|
|
# Setup fake exception for testing without pyrax
|
|
class NotFound(Exception):
|
|
pass
|
|
PYRAX_INSTALLED = False
|
|
|
|
|
|
def lb_immutable(exc):
|
|
return 'immutable' in six.text_type(exc)
|
|
|
|
|
|
class LoadbalancerDeleted(exception.HeatException):
|
|
msg_fmt = _("The Load Balancer (ID %(lb_id)s) has been deleted.")
|
|
|
|
|
|
class NodeNotFound(exception.HeatException):
|
|
msg_fmt = _("Node (ID %(node_id)s) not found on Load Balancer "
|
|
"(ID %(lb_id)s).")
|
|
|
|
|
|
class LBNode(resource.Resource):
|
|
"""Represents a single node of a Rackspace Cloud Load Balancer"""
|
|
|
|
default_client_name = 'cloud_lb'
|
|
|
|
_CONDITIONS = (
|
|
ENABLED, DISABLED, DRAINING,
|
|
) = (
|
|
'ENABLED', 'DISABLED', 'DRAINING',
|
|
)
|
|
|
|
_NODE_KEYS = (
|
|
ADDRESS, PORT, CONDITION, TYPE, WEIGHT
|
|
) = (
|
|
'address', 'port', 'condition', 'type', 'weight'
|
|
)
|
|
|
|
_OTHER_KEYS = (
|
|
LOAD_BALANCER, DRAINING_TIMEOUT
|
|
) = (
|
|
'load_balancer', 'draining_timeout'
|
|
)
|
|
|
|
PROPERTIES = _NODE_KEYS + _OTHER_KEYS
|
|
|
|
properties_schema = {
|
|
LOAD_BALANCER: properties.Schema(
|
|
properties.Schema.STRING,
|
|
_("The ID of the load balancer to associate the node with."),
|
|
required=True
|
|
),
|
|
DRAINING_TIMEOUT: properties.Schema(
|
|
properties.Schema.INTEGER,
|
|
_("The time to wait, in seconds, for the node to drain before it "
|
|
"is deleted."),
|
|
default=0,
|
|
constraints=[
|
|
constraints.Range(min=0)
|
|
],
|
|
update_allowed=True
|
|
),
|
|
ADDRESS: properties.Schema(
|
|
properties.Schema.STRING,
|
|
_("IP address for the node."),
|
|
required=True
|
|
),
|
|
PORT: properties.Schema(
|
|
properties.Schema.INTEGER,
|
|
required=True
|
|
),
|
|
CONDITION: properties.Schema(
|
|
properties.Schema.STRING,
|
|
default=ENABLED,
|
|
constraints=[
|
|
constraints.AllowedValues(_CONDITIONS),
|
|
],
|
|
update_allowed=True
|
|
),
|
|
TYPE: properties.Schema(
|
|
properties.Schema.STRING,
|
|
constraints=[
|
|
constraints.AllowedValues(['PRIMARY',
|
|
'SECONDARY']),
|
|
],
|
|
update_allowed=True
|
|
),
|
|
WEIGHT: properties.Schema(
|
|
properties.Schema.NUMBER,
|
|
constraints=[
|
|
constraints.Range(1, 100),
|
|
],
|
|
update_allowed=True
|
|
),
|
|
}
|
|
|
|
def lb(self):
|
|
lb_id = self.properties.get(self.LOAD_BALANCER)
|
|
lb = self.client().get(lb_id)
|
|
|
|
if lb.status in ('DELETED', 'PENDING_DELETE'):
|
|
raise LoadbalancerDeleted(lb_id=lb.id)
|
|
|
|
return lb
|
|
|
|
def node(self, lb):
|
|
for node in getattr(lb, 'nodes', []):
|
|
if node.id == self.resource_id:
|
|
return node
|
|
raise NodeNotFound(node_id=self.resource_id, lb_id=lb.id)
|
|
|
|
def handle_create(self):
|
|
pass
|
|
|
|
def check_create_complete(self, *args):
|
|
node_args = {k: self.properties.get(k) for k in self._NODE_KEYS}
|
|
node = self.client().Node(**node_args)
|
|
|
|
try:
|
|
resp, body = self.lb().add_nodes([node])
|
|
except Exception as exc:
|
|
if lb_immutable(exc):
|
|
return False
|
|
raise
|
|
|
|
new_node = body['nodes'][0]
|
|
node_id = new_node['id']
|
|
|
|
self.resource_id_set(node_id)
|
|
return True
|
|
|
|
def handle_update(self, json_snippet, tmpl_diff, prop_diff):
|
|
return prop_diff
|
|
|
|
def check_update_complete(self, prop_diff):
|
|
node = self.node(self.lb())
|
|
is_complete = True
|
|
|
|
for key in self._NODE_KEYS:
|
|
if key in prop_diff and getattr(node, key, None) != prop_diff[key]:
|
|
setattr(node, key, prop_diff[key])
|
|
is_complete = False
|
|
|
|
if is_complete:
|
|
return True
|
|
|
|
try:
|
|
node.update()
|
|
except Exception as exc:
|
|
if lb_immutable(exc):
|
|
return False
|
|
raise
|
|
|
|
return False
|
|
|
|
def handle_delete(self):
|
|
return timeutils.utcnow()
|
|
|
|
def check_delete_complete(self, deleted_at):
|
|
if self.resource_id is None:
|
|
return True
|
|
|
|
try:
|
|
node = self.node(self.lb())
|
|
except (NotFound, LoadbalancerDeleted, NodeNotFound):
|
|
return True
|
|
|
|
if isinstance(deleted_at, six.string_types):
|
|
deleted_at = timeutils.parse_isotime(deleted_at)
|
|
|
|
deleted_at = timeutils.normalize_time(deleted_at)
|
|
waited = timeutils.utcnow() - deleted_at
|
|
timeout_secs = self.properties[self.DRAINING_TIMEOUT]
|
|
timeout_secs = datetime.timedelta(seconds=timeout_secs)
|
|
|
|
if waited > timeout_secs:
|
|
try:
|
|
node.delete()
|
|
except NotFound:
|
|
return True
|
|
except Exception as exc:
|
|
if lb_immutable(exc):
|
|
return False
|
|
raise
|
|
elif node.condition != self.DRAINING:
|
|
node.condition = self.DRAINING
|
|
try:
|
|
node.update()
|
|
except Exception as exc:
|
|
if lb_immutable(exc):
|
|
return False
|
|
raise
|
|
|
|
return False
|
|
|
|
|
|
def resource_mapping():
|
|
return {'Rackspace::Cloud::LBNode': LBNode}
|
|
|
|
|
|
def available_resource_mapping():
|
|
if PYRAX_INSTALLED:
|
|
return resource_mapping()
|
|
return {}
|