heat/heat/engine/resources/openstack/neutron/loadbalancer.py

740 lines
26 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 six
from heat.common import exception
from heat.common.i18n import _
from heat.engine import attributes
from heat.engine import constraints
from heat.engine import properties
from heat.engine import resource
from heat.engine.resources.openstack.neutron import neutron
from heat.engine import support
class HealthMonitor(neutron.NeutronResource):
"""
A resource for managing health monitors for load balancers in Neutron.
"""
PROPERTIES = (
DELAY, TYPE, MAX_RETRIES, TIMEOUT, ADMIN_STATE_UP,
HTTP_METHOD, EXPECTED_CODES, URL_PATH,
) = (
'delay', 'type', 'max_retries', 'timeout', 'admin_state_up',
'http_method', 'expected_codes', 'url_path',
)
ATTRIBUTES = (
ADMIN_STATE_UP_ATTR, DELAY_ATTR, EXPECTED_CODES_ATTR, HTTP_METHOD_ATTR,
MAX_RETRIES_ATTR, TIMEOUT_ATTR, TYPE_ATTR, URL_PATH_ATTR, TENANT_ID,
SHOW,
) = (
'admin_state_up', 'delay', 'expected_codes', 'http_method',
'max_retries', 'timeout', 'type', 'url_path', 'tenant_id',
'show',
)
properties_schema = {
DELAY: properties.Schema(
properties.Schema.INTEGER,
_('The minimum time in seconds between regular connections of '
'the member.'),
required=True,
update_allowed=True
),
TYPE: properties.Schema(
properties.Schema.STRING,
_('One of predefined health monitor types.'),
required=True,
constraints=[
constraints.AllowedValues(['PING', 'TCP', 'HTTP', 'HTTPS']),
]
),
MAX_RETRIES: properties.Schema(
properties.Schema.INTEGER,
_('Number of permissible connection failures before changing the '
'member status to INACTIVE.'),
required=True,
update_allowed=True
),
TIMEOUT: properties.Schema(
properties.Schema.INTEGER,
_('Maximum number of seconds for a monitor to wait for a '
'connection to be established before it times out.'),
required=True,
update_allowed=True
),
ADMIN_STATE_UP: properties.Schema(
properties.Schema.BOOLEAN,
_('The administrative state of the health monitor.'),
default=True,
update_allowed=True
),
HTTP_METHOD: properties.Schema(
properties.Schema.STRING,
_('The HTTP method used for requests by the monitor of type '
'HTTP.'),
update_allowed=True
),
EXPECTED_CODES: properties.Schema(
properties.Schema.STRING,
_('The list of HTTP status codes expected in response from the '
'member to declare it healthy.'),
update_allowed=True
),
URL_PATH: properties.Schema(
properties.Schema.STRING,
_('The HTTP path used in the HTTP request used by the monitor to '
'test a member health.'),
update_allowed=True
),
}
attributes_schema = {
ADMIN_STATE_UP_ATTR: attributes.Schema(
_('The administrative state of this health monitor.')
),
DELAY_ATTR: attributes.Schema(
_('The minimum time in seconds between regular connections '
'of the member.')
),
EXPECTED_CODES_ATTR: attributes.Schema(
_('The list of HTTP status codes expected in response '
'from the member to declare it healthy.')
),
HTTP_METHOD_ATTR: attributes.Schema(
_('The HTTP method used for requests by the monitor of type HTTP.')
),
MAX_RETRIES_ATTR: attributes.Schema(
_('Number of permissible connection failures before changing '
'the member status to INACTIVE.')
),
TIMEOUT_ATTR: attributes.Schema(
_('Maximum number of seconds for a monitor to wait for a '
'connection to be established before it times out.')
),
TYPE_ATTR: attributes.Schema(
_('One of predefined health monitor types.')
),
URL_PATH_ATTR: attributes.Schema(
_('The HTTP path used in the HTTP request used by the monitor '
'to test a member health.')
),
TENANT_ID: attributes.Schema(
_('Tenant owning the health monitor.')
),
SHOW: attributes.Schema(
_('All attributes.')
),
}
def handle_create(self):
properties = self.prepare_properties(
self.properties,
self.physical_resource_name())
health_monitor = self.neutron().create_health_monitor(
{'health_monitor': properties})['health_monitor']
self.resource_id_set(health_monitor['id'])
def _show_resource(self):
return self.neutron().show_health_monitor(
self.resource_id)['health_monitor']
def handle_update(self, json_snippet, tmpl_diff, prop_diff):
if prop_diff:
self.neutron().update_health_monitor(
self.resource_id, {'health_monitor': prop_diff})
def handle_delete(self):
try:
self.neutron().delete_health_monitor(self.resource_id)
except Exception as ex:
self.client_plugin().ignore_not_found(ex)
else:
return True
class Pool(neutron.NeutronResource):
"""
A resource for managing load balancer pools in Neutron.
"""
PROPERTIES = (
PROTOCOL, SUBNET_ID, SUBNET, LB_METHOD, NAME, DESCRIPTION,
ADMIN_STATE_UP, VIP, MONITORS,
) = (
'protocol', 'subnet_id', 'subnet', 'lb_method', 'name', 'description',
'admin_state_up', 'vip', 'monitors',
)
_VIP_KEYS = (
VIP_NAME, VIP_DESCRIPTION, VIP_SUBNET, VIP_ADDRESS,
VIP_CONNECTION_LIMIT, VIP_PROTOCOL_PORT,
VIP_SESSION_PERSISTENCE, VIP_ADMIN_STATE_UP,
) = (
'name', 'description', 'subnet', 'address',
'connection_limit', 'protocol_port',
'session_persistence', 'admin_state_up',
)
_VIP_SESSION_PERSISTENCE_KEYS = (
VIP_SESSION_PERSISTENCE_TYPE, VIP_SESSION_PERSISTENCE_COOKIE_NAME,
) = (
'type', 'cookie_name',
)
ATTRIBUTES = (
ADMIN_STATE_UP_ATTR, NAME_ATTR, PROTOCOL_ATTR, SUBNET_ID_ATTR,
LB_METHOD_ATTR, DESCRIPTION_ATTR, TENANT_ID, VIP_ATTR,
) = (
'admin_state_up', 'name', 'protocol', 'subnet_id',
'lb_method', 'description', 'tenant_id', 'vip',
)
properties_schema = {
PROTOCOL: properties.Schema(
properties.Schema.STRING,
_('Protocol for balancing.'),
required=True,
constraints=[
constraints.AllowedValues(['TCP', 'HTTP', 'HTTPS']),
]
),
SUBNET_ID: properties.Schema(
properties.Schema.STRING,
support_status=support.SupportStatus(
status=support.DEPRECATED,
message=_('Use property %s.') % SUBNET,
version='2014.2'),
constraints=[
constraints.CustomConstraint('neutron.subnet')
]
),
SUBNET: properties.Schema(
properties.Schema.STRING,
_('The subnet for the port on which the members '
'of the pool will be connected.'),
support_status=support.SupportStatus(version='2014.2'),
constraints=[
constraints.CustomConstraint('neutron.subnet')
]
),
LB_METHOD: properties.Schema(
properties.Schema.STRING,
_('The algorithm used to distribute load between the members of '
'the pool.'),
required=True,
constraints=[
constraints.AllowedValues(['ROUND_ROBIN',
'LEAST_CONNECTIONS', 'SOURCE_IP']),
],
update_allowed=True
),
NAME: properties.Schema(
properties.Schema.STRING,
_('Name of the pool.')
),
DESCRIPTION: properties.Schema(
properties.Schema.STRING,
_('Description of the pool.'),
update_allowed=True
),
ADMIN_STATE_UP: properties.Schema(
properties.Schema.BOOLEAN,
_('The administrative state of this pool.'),
default=True,
update_allowed=True
),
VIP: properties.Schema(
properties.Schema.MAP,
_('IP address and port of the pool.'),
schema={
VIP_NAME: properties.Schema(
properties.Schema.STRING,
_('Name of the vip.')
),
VIP_DESCRIPTION: properties.Schema(
properties.Schema.STRING,
_('Description of the vip.')
),
VIP_SUBNET: properties.Schema(
properties.Schema.STRING,
_('Subnet of the vip.'),
constraints=[
constraints.CustomConstraint('neutron.subnet')
]
),
VIP_ADDRESS: properties.Schema(
properties.Schema.STRING,
_('IP address of the vip.')
),
VIP_CONNECTION_LIMIT: properties.Schema(
properties.Schema.INTEGER,
_('The maximum number of connections per second '
'allowed for the vip.')
),
VIP_PROTOCOL_PORT: properties.Schema(
properties.Schema.INTEGER,
_('TCP port on which to listen for client traffic '
'that is associated with the vip address.'),
required=True
),
VIP_SESSION_PERSISTENCE: properties.Schema(
properties.Schema.MAP,
_('Configuration of session persistence.'),
schema={
VIP_SESSION_PERSISTENCE_TYPE: properties.Schema(
properties.Schema.STRING,
_('Method of implementation of session '
'persistence feature.'),
required=True,
constraints=[constraints.AllowedValues(
['SOURCE_IP', 'HTTP_COOKIE', 'APP_COOKIE']
)]
),
VIP_SESSION_PERSISTENCE_COOKIE_NAME: properties.Schema(
properties.Schema.STRING,
_('Name of the cookie, '
'required if type is APP_COOKIE.')
)
}
),
VIP_ADMIN_STATE_UP: properties.Schema(
properties.Schema.BOOLEAN,
_('The administrative state of this vip.'),
default=True
),
},
required=True
),
MONITORS: properties.Schema(
properties.Schema.LIST,
_('List of health monitors associated with the pool.'),
default=[],
update_allowed=True
),
}
attributes_schema = {
ADMIN_STATE_UP_ATTR: attributes.Schema(
_('The administrative state of this pool.')
),
NAME_ATTR: attributes.Schema(
_('Name of the pool.')
),
PROTOCOL_ATTR: attributes.Schema(
_('Protocol to balance.')
),
SUBNET_ID_ATTR: attributes.Schema(
_('The subnet for the port on which the members of the pool '
'will be connected.')
),
LB_METHOD_ATTR: attributes.Schema(
_('The algorithm used to distribute load between the members '
'of the pool.')
),
DESCRIPTION_ATTR: attributes.Schema(
_('Description of the pool.')
),
TENANT_ID: attributes.Schema(
_('Tenant owning the pool.')
),
VIP_ATTR: attributes.Schema(
_('Vip associated with the pool.')
),
}
def validate(self):
res = super(Pool, self).validate()
if res:
return res
self._validate_depr_property_required(
self.properties, self.SUBNET, self.SUBNET_ID)
session_p = self.properties[self.VIP].get(self.VIP_SESSION_PERSISTENCE)
if session_p is None:
# session persistence is not configured, skip validation
return
persistence_type = session_p[self.VIP_SESSION_PERSISTENCE_TYPE]
if persistence_type == 'APP_COOKIE':
if session_p.get(self.VIP_SESSION_PERSISTENCE_COOKIE_NAME):
return
msg = _('Property cookie_name is required, when '
'session_persistence type is set to APP_COOKIE.')
raise exception.StackValidationFailed(message=msg)
def handle_create(self):
properties = self.prepare_properties(
self.properties,
self.physical_resource_name())
self.client_plugin().resolve_subnet(
properties, self.SUBNET, 'subnet_id')
vip_properties = properties.pop(self.VIP)
monitors = properties.pop(self.MONITORS)
client = self.neutron()
pool = client.create_pool({'pool': properties})['pool']
self.resource_id_set(pool['id'])
for monitor in monitors:
client.associate_health_monitor(
pool['id'], {'health_monitor': {'id': monitor}})
vip_arguments = self.prepare_properties(
vip_properties,
'%s.vip' % (self.name,))
session_p = vip_arguments.get(self.VIP_SESSION_PERSISTENCE)
if session_p is not None:
prepared_props = self.prepare_properties(session_p, None)
vip_arguments['session_persistence'] = prepared_props
vip_arguments['protocol'] = self.properties[self.PROTOCOL]
if vip_arguments.get(self.VIP_SUBNET) is None:
vip_arguments['subnet_id'] = properties[self.SUBNET_ID]
else:
vip_arguments['subnet_id'] = self.client_plugin().resolve_subnet(
vip_arguments, self.VIP_SUBNET, 'subnet_id')
vip_arguments['pool_id'] = pool['id']
vip = client.create_vip({'vip': vip_arguments})['vip']
self.metadata_set({'vip': vip['id']})
def _show_resource(self):
return self.neutron().show_pool(self.resource_id)['pool']
def check_create_complete(self, data):
attributes = self._show_resource()
status = attributes['status']
if status == 'PENDING_CREATE':
return False
elif status == 'ACTIVE':
vip_attributes = self.neutron().show_vip(
self.metadata_get()['vip'])['vip']
vip_status = vip_attributes['status']
if vip_status == 'PENDING_CREATE':
return False
if vip_status == 'ACTIVE':
return True
if vip_status == 'ERROR':
raise resource.ResourceInError(
resource_status=vip_status,
status_reason=_('error in vip'))
raise resource.ResourceUnknownStatus(
resource_status=vip_status,
result=_('Pool creation failed due to vip'))
elif status == 'ERROR':
raise resource.ResourceInError(
resource_status=status,
status_reason=_('error in pool'))
else:
raise resource.ResourceUnknownStatus(
resource_status=status,
result=_('Pool creation failed'))
def handle_update(self, json_snippet, tmpl_diff, prop_diff):
if prop_diff:
client = self.neutron()
if self.MONITORS in prop_diff:
monitors = set(prop_diff.pop(self.MONITORS))
old_monitors = set(self.properties[self.MONITORS])
for monitor in old_monitors - monitors:
client.disassociate_health_monitor(self.resource_id,
monitor)
for monitor in monitors - old_monitors:
client.associate_health_monitor(
self.resource_id, {'health_monitor': {'id': monitor}})
if prop_diff:
client.update_pool(self.resource_id, {'pool': prop_diff})
def _resolve_attribute(self, name):
if name == self.VIP_ATTR:
return self.neutron().show_vip(self.metadata_get()['vip'])['vip']
return super(Pool, self)._resolve_attribute(name)
def handle_delete(self):
class PoolDeleteProgress(object):
def __init__(self, val=False):
self.pool = {'delete_called': val,
'deleted': val}
self.vip = {'delete_called': val,
'deleted': val}
if not self.resource_id:
progress = PoolDeleteProgress(True)
return progress
progress = PoolDeleteProgress()
if not self.metadata_get():
progress.vip['delete_called'] = True
progress.vip['deleted'] = True
return progress
def _delete_vip(self):
return self._not_found_in_call(
self.neutron().delete_vip, self.metadata_get()['vip'])
def _check_vip_deleted(self):
return self._not_found_in_call(
self.neutron().show_vip, self.metadata_get()['vip'])
def _delete_pool(self):
return self._not_found_in_call(
self.neutron().delete_pool, self.resource_id)
def check_delete_complete(self, prg):
if not prg.vip['delete_called']:
prg.vip['deleted'] = self._delete_vip()
prg.vip['delete_called'] = True
return False
if not prg.vip['deleted']:
prg.vip['deleted'] = self._check_vip_deleted()
return False
if not prg.pool['delete_called']:
prg.pool['deleted'] = self._delete_pool()
prg.pool['delete_called'] = True
return prg.pool['deleted']
if not prg.pool['deleted']:
prg.pool['deleted'] = super(Pool, self).check_delete_complete(True)
return prg.pool['deleted']
return True
class PoolMember(neutron.NeutronResource):
"""
A resource to handle load balancer members.
"""
support_status = support.SupportStatus(version='2014.1')
PROPERTIES = (
POOL_ID, ADDRESS, PROTOCOL_PORT, WEIGHT, ADMIN_STATE_UP,
) = (
'pool_id', 'address', 'protocol_port', 'weight', 'admin_state_up',
)
ATTRIBUTES = (
ADMIN_STATE_UP_ATTR, TENANT_ID, WEIGHT_ATTR, ADDRESS_ATTR,
POOL_ID_ATTR, PROTOCOL_PORT_ATTR, SHOW,
) = (
'admin_state_up', 'tenant_id', 'weight', 'address',
'pool_id', 'protocol_port', 'show',
)
properties_schema = {
POOL_ID: properties.Schema(
properties.Schema.STRING,
_('The ID of the load balancing pool.'),
required=True,
update_allowed=True
),
ADDRESS: properties.Schema(
properties.Schema.STRING,
_('IP address of the pool member on the pool network.'),
required=True
),
PROTOCOL_PORT: properties.Schema(
properties.Schema.INTEGER,
_('TCP port on which the pool member listens for requests or '
'connections.'),
required=True,
constraints=[
constraints.Range(0, 65535),
]
),
WEIGHT: properties.Schema(
properties.Schema.INTEGER,
_('Weight of pool member in the pool (default to 1).'),
constraints=[
constraints.Range(0, 256),
],
update_allowed=True
),
ADMIN_STATE_UP: properties.Schema(
properties.Schema.BOOLEAN,
_('The administrative state of the pool member.'),
default=True
),
}
attributes_schema = {
ADMIN_STATE_UP_ATTR: attributes.Schema(
_('The administrative state of this pool member.')
),
TENANT_ID: attributes.Schema(
_('Tenant owning the pool member.')
),
WEIGHT_ATTR: attributes.Schema(
_('Weight of the pool member in the pool.')
),
ADDRESS_ATTR: attributes.Schema(
_('IP address of the pool member.')
),
POOL_ID_ATTR: attributes.Schema(
_('The ID of the load balancing pool.')
),
PROTOCOL_PORT_ATTR: attributes.Schema(
_('TCP port on which the pool member listens for requests or '
'connections.')
),
SHOW: attributes.Schema(
_('All attributes.')
),
}
def handle_create(self):
pool = self.properties[self.POOL_ID]
client = self.neutron()
protocol_port = self.properties[self.PROTOCOL_PORT]
address = self.properties[self.ADDRESS]
admin_state_up = self.properties[self.ADMIN_STATE_UP]
weight = self.properties[self.WEIGHT]
params = {
'pool_id': pool,
'address': address,
'protocol_port': protocol_port,
'admin_state_up': admin_state_up
}
if weight is not None:
params['weight'] = weight
member = client.create_member({'member': params})['member']
self.resource_id_set(member['id'])
def _show_resource(self):
return self.neutron().show_member(self.resource_id)['member']
def handle_update(self, json_snippet, tmpl_diff, prop_diff):
if prop_diff:
self.neutron().update_member(
self.resource_id, {'member': prop_diff})
def handle_delete(self):
client = self.neutron()
try:
client.delete_member(self.resource_id)
except Exception as ex:
self.client_plugin().ignore_not_found(ex)
else:
return True
class LoadBalancer(resource.Resource):
"""
A resource to link a neutron pool with servers.
"""
PROPERTIES = (
POOL_ID, PROTOCOL_PORT, MEMBERS,
) = (
'pool_id', 'protocol_port', 'members',
)
properties_schema = {
POOL_ID: properties.Schema(
properties.Schema.STRING,
_('The ID of the load balancing pool.'),
required=True,
update_allowed=True
),
PROTOCOL_PORT: properties.Schema(
properties.Schema.INTEGER,
_('Port number on which the servers are running on the members.'),
required=True
),
MEMBERS: properties.Schema(
properties.Schema.LIST,
_('The list of Nova server IDs load balanced.'),
update_allowed=True
),
}
default_client_name = 'neutron'
def handle_create(self):
pool = self.properties[self.POOL_ID]
client = self.neutron()
protocol_port = self.properties[self.PROTOCOL_PORT]
for member in self.properties[self.MEMBERS] or []:
address = self.client_plugin('nova').server_to_ipaddress(member)
lb_member = client.create_member({
'member': {
'pool_id': pool,
'address': address,
'protocol_port': protocol_port}})['member']
self.data_set(member, lb_member['id'])
def handle_update(self, json_snippet, tmpl_diff, prop_diff):
new_props = json_snippet.properties(self.properties_schema,
self.context)
# Valid use cases are:
# - Membership controlled by members property in template
# - Empty members property in template; membership controlled by
# "updates" triggered from autoscaling group.
# Mixing the two will lead to undefined behaviour.
if (self.MEMBERS in prop_diff and
(self.properties[self.MEMBERS] is not None or
new_props[self.MEMBERS] is not None)):
members = set(new_props[self.MEMBERS] or [])
rd_members = self.data()
old_members = set(six.iterkeys(rd_members))
client = self.neutron()
for member in old_members - members:
member_id = rd_members[member]
try:
client.delete_member(member_id)
except Exception as ex:
self.client_plugin().ignore_not_found(ex)
self.data_delete(member)
pool = self.properties[self.POOL_ID]
protocol_port = self.properties[self.PROTOCOL_PORT]
for member in members - old_members:
address = self.client_plugin('nova').server_to_ipaddress(
member)
lb_member = client.create_member({
'member': {
'pool_id': pool,
'address': address,
'protocol_port': protocol_port}})['member']
self.data_set(member, lb_member['id'])
def handle_delete(self):
client = self.neutron()
# FIXME(pshchelo): this deletes members in a tight loop,
# so is prone to OverLimit bug similar to LP 1265937
for member, member_id in self.data().items():
try:
client.delete_member(member_id)
except Exception as ex:
self.client_plugin().ignore_not_found(ex)
self.data_delete(member)
def resource_mapping():
return {
'OS::Neutron::HealthMonitor': HealthMonitor,
'OS::Neutron::Pool': Pool,
'OS::Neutron::PoolMember': PoolMember,
'OS::Neutron::LoadBalancer': LoadBalancer,
}