ec5da3cc2b
This driver for the Neutron LBaaS plugin allows for using the Citrix NetScaler loadbalancing devices to provide Neutron LBaaS functionality in OpenStack. Change-Id: Ibfeb54c4402943fb3696a1c599fa373e42e520d4 Implements: blueprint netscaler-lbaas-driver
490 lines
21 KiB
Python
490 lines
21 KiB
Python
# Copyright 2014 Citrix Systems, Inc.
|
|
#
|
|
# 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.
|
|
|
|
from oslo.config import cfg
|
|
|
|
from neutron.api.v2 import attributes
|
|
from neutron.db.loadbalancer import loadbalancer_db
|
|
from neutron.openstack.common import log as logging
|
|
from neutron.plugins.common import constants
|
|
from neutron.services.loadbalancer.drivers import abstract_driver
|
|
from neutron.services.loadbalancer.drivers.netscaler import ncc_client
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
|
|
NETSCALER_CC_OPTS = [
|
|
cfg.StrOpt(
|
|
'netscaler_ncc_uri',
|
|
help=_('The URL to reach the NetScaler Control Center Server.'),
|
|
),
|
|
cfg.StrOpt(
|
|
'netscaler_ncc_username',
|
|
help=_('Username to login to the NetScaler Control Center Server.'),
|
|
),
|
|
cfg.StrOpt(
|
|
'netscaler_ncc_password',
|
|
help=_('Password to login to the NetScaler Control Center Server.'),
|
|
)
|
|
]
|
|
|
|
cfg.CONF.register_opts(NETSCALER_CC_OPTS, 'netscaler_driver')
|
|
|
|
VIPS_RESOURCE = 'vips'
|
|
VIP_RESOURCE = 'vip'
|
|
POOLS_RESOURCE = 'pools'
|
|
POOL_RESOURCE = 'pool'
|
|
POOLMEMBERS_RESOURCE = 'members'
|
|
POOLMEMBER_RESOURCE = 'member'
|
|
MONITORS_RESOURCE = 'healthmonitors'
|
|
MONITOR_RESOURCE = 'healthmonitor'
|
|
POOLSTATS_RESOURCE = 'statistics'
|
|
PROV_SEGMT_ID = 'provider:segmentation_id'
|
|
PROV_NET_TYPE = 'provider:network_type'
|
|
DRIVER_NAME = 'netscaler_driver'
|
|
|
|
|
|
class NetScalerPluginDriver(abstract_driver.LoadBalancerAbstractDriver):
|
|
|
|
"""NetScaler LBaaS Plugin driver class."""
|
|
|
|
def __init__(self, plugin):
|
|
self.plugin = plugin
|
|
ncc_uri = cfg.CONF.netscaler_driver.netscaler_ncc_uri
|
|
ncc_username = cfg.CONF.netscaler_driver.netscaler_ncc_username
|
|
ncc_password = cfg.CONF.netscaler_driver.netscaler_ncc_password
|
|
self.client = ncc_client.NSClient(ncc_uri,
|
|
ncc_username,
|
|
ncc_password)
|
|
|
|
def create_vip(self, context, vip):
|
|
"""Create a vip on a NetScaler device."""
|
|
network_info = self._get_vip_network_info(context, vip)
|
|
ncc_vip = self._prepare_vip_for_creation(vip)
|
|
ncc_vip = dict(ncc_vip.items() + network_info.items())
|
|
msg = _("NetScaler driver vip creation: %s") % repr(ncc_vip)
|
|
LOG.debug(msg)
|
|
status = constants.ACTIVE
|
|
try:
|
|
self.client.create_resource(context.tenant_id, VIPS_RESOURCE,
|
|
VIP_RESOURCE, ncc_vip)
|
|
except ncc_client.NCCException:
|
|
status = constants.ERROR
|
|
self.plugin.update_status(context, loadbalancer_db.Vip, vip["id"],
|
|
status)
|
|
|
|
def update_vip(self, context, old_vip, vip):
|
|
"""Update a vip on a NetScaler device."""
|
|
update_vip = self._prepare_vip_for_update(vip)
|
|
resource_path = "%s/%s" % (VIPS_RESOURCE, vip["id"])
|
|
msg = (_("NetScaler driver vip %(vip_id)s update: %(vip_obj)s") %
|
|
{"vip_id": vip["id"], "vip_obj": repr(vip)})
|
|
LOG.debug(msg)
|
|
status = constants.ACTIVE
|
|
try:
|
|
self.client.update_resource(context.tenant_id, resource_path,
|
|
VIP_RESOURCE, update_vip)
|
|
except ncc_client.NCCException:
|
|
status = constants.ERROR
|
|
self.plugin.update_status(context, loadbalancer_db.Vip, old_vip["id"],
|
|
status)
|
|
|
|
def delete_vip(self, context, vip):
|
|
"""Delete a vip on a NetScaler device."""
|
|
resource_path = "%s/%s" % (VIPS_RESOURCE, vip["id"])
|
|
msg = _("NetScaler driver vip removal: %s") % vip["id"]
|
|
LOG.debug(msg)
|
|
try:
|
|
self.client.remove_resource(context.tenant_id, resource_path)
|
|
except ncc_client.NCCException:
|
|
self.plugin.update_status(context, loadbalancer_db.Vip,
|
|
vip["id"],
|
|
constants.ERROR)
|
|
else:
|
|
self.plugin._delete_db_vip(context, vip['id'])
|
|
|
|
def create_pool(self, context, pool):
|
|
"""Create a pool on a NetScaler device."""
|
|
network_info = self._get_pool_network_info(context, pool)
|
|
#allocate a snat port/ipaddress on the subnet if one doesn't exist
|
|
self._create_snatport_for_subnet_if_not_exists(context,
|
|
pool['tenant_id'],
|
|
pool['subnet_id'],
|
|
network_info)
|
|
ncc_pool = self._prepare_pool_for_creation(pool)
|
|
ncc_pool = dict(ncc_pool.items() + network_info.items())
|
|
msg = _("NetScaler driver pool creation: %s") % repr(ncc_pool)
|
|
LOG.debug(msg)
|
|
status = constants.ACTIVE
|
|
try:
|
|
self.client.create_resource(context.tenant_id, POOLS_RESOURCE,
|
|
POOL_RESOURCE, ncc_pool)
|
|
except ncc_client.NCCException:
|
|
status = constants.ERROR
|
|
self.plugin.update_status(context, loadbalancer_db.Pool,
|
|
ncc_pool["id"], status)
|
|
|
|
def update_pool(self, context, old_pool, pool):
|
|
"""Update a pool on a NetScaler device."""
|
|
ncc_pool = self._prepare_pool_for_update(pool)
|
|
resource_path = "%s/%s" % (POOLS_RESOURCE, old_pool["id"])
|
|
msg = (_("NetScaler driver pool %(pool_id)s update: %(pool_obj)s") %
|
|
{"pool_id": old_pool["id"], "pool_obj": repr(ncc_pool)})
|
|
LOG.debug(msg)
|
|
status = constants.ACTIVE
|
|
try:
|
|
self.client.update_resource(context.tenant_id, resource_path,
|
|
POOL_RESOURCE, ncc_pool)
|
|
except ncc_client.NCCException:
|
|
status = constants.ERROR
|
|
self.plugin.update_status(context, loadbalancer_db.Pool,
|
|
old_pool["id"], status)
|
|
|
|
def delete_pool(self, context, pool):
|
|
"""Delete a pool on a NetScaler device."""
|
|
resource_path = "%s/%s" % (POOLS_RESOURCE, pool['id'])
|
|
msg = _("NetScaler driver pool removal: %s") % pool["id"]
|
|
LOG.debug(msg)
|
|
try:
|
|
self.client.remove_resource(context.tenant_id, resource_path)
|
|
except ncc_client.NCCException:
|
|
self.plugin.update_status(context, loadbalancer_db.Pool,
|
|
pool["id"],
|
|
constants.ERROR)
|
|
else:
|
|
self.plugin._delete_db_pool(context, pool['id'])
|
|
self._remove_snatport_for_subnet_if_not_used(context,
|
|
pool['tenant_id'],
|
|
pool['subnet_id'])
|
|
|
|
def create_member(self, context, member):
|
|
"""Create a pool member on a NetScaler device."""
|
|
ncc_member = self._prepare_member_for_creation(member)
|
|
msg = (_("NetScaler driver poolmember creation: %s") %
|
|
repr(ncc_member))
|
|
LOG.info(msg)
|
|
status = constants.ACTIVE
|
|
try:
|
|
self.client.create_resource(context.tenant_id,
|
|
POOLMEMBERS_RESOURCE,
|
|
POOLMEMBER_RESOURCE,
|
|
ncc_member)
|
|
except ncc_client.NCCException:
|
|
status = constants.ERROR
|
|
self.plugin.update_status(context, loadbalancer_db.Member,
|
|
member["id"], status)
|
|
|
|
def update_member(self, context, old_member, member):
|
|
"""Update a pool member on a NetScaler device."""
|
|
ncc_member = self._prepare_member_for_update(member)
|
|
resource_path = "%s/%s" % (POOLMEMBERS_RESOURCE, old_member["id"])
|
|
msg = (_("NetScaler driver poolmember %(member_id)s update:"
|
|
" %(member_obj)s") %
|
|
{"member_id": old_member["id"],
|
|
"member_obj": repr(ncc_member)})
|
|
LOG.debug(msg)
|
|
status = constants.ACTIVE
|
|
try:
|
|
self.client.update_resource(context.tenant_id, resource_path,
|
|
POOLMEMBER_RESOURCE, ncc_member)
|
|
except ncc_client.NCCException:
|
|
status = constants.ERROR
|
|
self.plugin.update_status(context, loadbalancer_db.Member,
|
|
old_member["id"], status)
|
|
|
|
def delete_member(self, context, member):
|
|
"""Delete a pool member on a NetScaler device."""
|
|
resource_path = "%s/%s" % (POOLMEMBERS_RESOURCE, member['id'])
|
|
msg = (_("NetScaler driver poolmember removal: %s") %
|
|
member["id"])
|
|
LOG.debug(msg)
|
|
try:
|
|
self.client.remove_resource(context.tenant_id, resource_path)
|
|
except ncc_client.NCCException:
|
|
self.plugin.update_status(context, loadbalancer_db.Member,
|
|
member["id"],
|
|
constants.ERROR)
|
|
else:
|
|
self.plugin._delete_db_member(context, member['id'])
|
|
|
|
def create_pool_health_monitor(self, context, health_monitor, pool_id):
|
|
"""Create a pool health monitor on a NetScaler device."""
|
|
ncc_hm = self._prepare_healthmonitor_for_creation(health_monitor,
|
|
pool_id)
|
|
resource_path = "%s/%s/%s" % (POOLS_RESOURCE, pool_id,
|
|
MONITORS_RESOURCE)
|
|
msg = (_("NetScaler driver healthmonitor creation for pool %(pool_id)s"
|
|
": %(monitor_obj)s") %
|
|
{"pool_id": pool_id,
|
|
"monitor_obj": repr(ncc_hm)})
|
|
LOG.debug(msg)
|
|
status = constants.ACTIVE
|
|
try:
|
|
self.client.create_resource(context.tenant_id, resource_path,
|
|
MONITOR_RESOURCE,
|
|
ncc_hm)
|
|
except ncc_client.NCCException:
|
|
status = constants.ERROR
|
|
self.plugin.update_pool_health_monitor(context,
|
|
health_monitor['id'],
|
|
pool_id,
|
|
status, "")
|
|
|
|
def update_pool_health_monitor(self, context, old_health_monitor,
|
|
health_monitor, pool_id):
|
|
"""Update a pool health monitor on a NetScaler device."""
|
|
ncc_hm = self._prepare_healthmonitor_for_update(health_monitor)
|
|
resource_path = "%s/%s" % (MONITORS_RESOURCE,
|
|
old_health_monitor["id"])
|
|
msg = (_("NetScaler driver healthmonitor %(monitor_id)s update: "
|
|
"%(monitor_obj)s") %
|
|
{"monitor_id": old_health_monitor["id"],
|
|
"monitor_obj": repr(ncc_hm)})
|
|
LOG.debug(msg)
|
|
status = constants.ACTIVE
|
|
try:
|
|
self.client.update_resource(context.tenant_id, resource_path,
|
|
MONITOR_RESOURCE, ncc_hm)
|
|
except ncc_client.NCCException:
|
|
status = constants.ERROR
|
|
self.plugin.update_pool_health_monitor(context,
|
|
old_health_monitor['id'],
|
|
pool_id,
|
|
status, "")
|
|
|
|
def delete_pool_health_monitor(self, context, health_monitor, pool_id):
|
|
"""Delete a pool health monitor on a NetScaler device."""
|
|
resource_path = "%s/%s/%s/%s" % (POOLS_RESOURCE, pool_id,
|
|
MONITORS_RESOURCE,
|
|
health_monitor["id"])
|
|
msg = (_("NetScaler driver healthmonitor %(monitor_id)s"
|
|
"removal for pool %(pool_id)s") %
|
|
{"monitor_id": health_monitor["id"],
|
|
"pool_id": pool_id})
|
|
LOG.debug(msg)
|
|
try:
|
|
self.client.remove_resource(context.tenant_id, resource_path)
|
|
except ncc_client.NCCException:
|
|
self.plugin.update_pool_health_monitor(context,
|
|
health_monitor['id'],
|
|
pool_id,
|
|
constants.ERROR, "")
|
|
else:
|
|
self.plugin._delete_db_pool_health_monitor(context,
|
|
health_monitor['id'],
|
|
pool_id)
|
|
|
|
def stats(self, context, pool_id):
|
|
"""Retrieve pool statistics from the NetScaler device."""
|
|
resource_path = "%s/%s" % (POOLSTATS_RESOURCE, pool_id)
|
|
msg = _("NetScaler driver pool stats retrieval: %s") % pool_id
|
|
LOG.debug(msg)
|
|
try:
|
|
stats = self.client.retrieve_resource(context.tenant_id,
|
|
resource_path)[1]
|
|
except ncc_client.NCCException:
|
|
self.plugin.update_status(context, loadbalancer_db.Pool,
|
|
pool_id, constants.ERROR)
|
|
else:
|
|
return stats
|
|
|
|
def _prepare_vip_for_creation(self, vip):
|
|
creation_attrs = {
|
|
'id': vip['id'],
|
|
'tenant_id': vip['tenant_id'],
|
|
'protocol': vip['protocol'],
|
|
'address': vip['address'],
|
|
'protocol_port': vip['protocol_port'],
|
|
}
|
|
if 'session_persistence' in vip:
|
|
creation_attrs['session_persistence'] = vip['session_persistence']
|
|
update_attrs = self._prepare_vip_for_update(vip)
|
|
creation_attrs.update(update_attrs)
|
|
return creation_attrs
|
|
|
|
def _prepare_vip_for_update(self, vip):
|
|
return {
|
|
'name': vip['name'],
|
|
'description': vip['description'],
|
|
'pool_id': vip['pool_id'],
|
|
'connection_limit': vip['connection_limit'],
|
|
'admin_state_up': vip['admin_state_up']
|
|
}
|
|
|
|
def _prepare_pool_for_creation(self, pool):
|
|
creation_attrs = {
|
|
'id': pool['id'],
|
|
'tenant_id': pool['tenant_id'],
|
|
'vip_id': pool['vip_id'],
|
|
'protocol': pool['protocol'],
|
|
'subnet_id': pool['subnet_id'],
|
|
}
|
|
update_attrs = self._prepare_pool_for_update(pool)
|
|
creation_attrs.update(update_attrs)
|
|
return creation_attrs
|
|
|
|
def _prepare_pool_for_update(self, pool):
|
|
return {
|
|
'name': pool['name'],
|
|
'description': pool['description'],
|
|
'lb_method': pool['lb_method'],
|
|
'admin_state_up': pool['admin_state_up']
|
|
}
|
|
|
|
def _prepare_member_for_creation(self, member):
|
|
creation_attrs = {
|
|
'id': member['id'],
|
|
'tenant_id': member['tenant_id'],
|
|
'address': member['address'],
|
|
'protocol_port': member['protocol_port'],
|
|
}
|
|
update_attrs = self._prepare_member_for_update(member)
|
|
creation_attrs.update(update_attrs)
|
|
return creation_attrs
|
|
|
|
def _prepare_member_for_update(self, member):
|
|
return {
|
|
'pool_id': member['pool_id'],
|
|
'weight': member['weight'],
|
|
'admin_state_up': member['admin_state_up']
|
|
}
|
|
|
|
def _prepare_healthmonitor_for_creation(self, health_monitor, pool_id):
|
|
creation_attrs = {
|
|
'id': health_monitor['id'],
|
|
'tenant_id': health_monitor['tenant_id'],
|
|
'type': health_monitor['type'],
|
|
}
|
|
update_attrs = self._prepare_healthmonitor_for_update(health_monitor)
|
|
creation_attrs.update(update_attrs)
|
|
return creation_attrs
|
|
|
|
def _prepare_healthmonitor_for_update(self, health_monitor):
|
|
ncc_hm = {
|
|
'delay': health_monitor['delay'],
|
|
'timeout': health_monitor['timeout'],
|
|
'max_retries': health_monitor['max_retries'],
|
|
'admin_state_up': health_monitor['admin_state_up']
|
|
}
|
|
if health_monitor['type'] in ['HTTP', 'HTTPS']:
|
|
ncc_hm['http_method'] = health_monitor['http_method']
|
|
ncc_hm['url_path'] = health_monitor['url_path']
|
|
ncc_hm['expected_codes'] = health_monitor['expected_codes']
|
|
return ncc_hm
|
|
|
|
def _get_network_info(self, context, entity):
|
|
network_info = {}
|
|
subnet_id = entity['subnet_id']
|
|
subnet = self.plugin._core_plugin.get_subnet(context, subnet_id)
|
|
network_id = subnet['network_id']
|
|
network = self.plugin._core_plugin.get_network(context, network_id)
|
|
network_info['network_id'] = network_id
|
|
network_info['subnet_id'] = subnet_id
|
|
if PROV_NET_TYPE in network:
|
|
network_info['network_type'] = network[PROV_NET_TYPE]
|
|
if PROV_SEGMT_ID in network:
|
|
network_info['segmentation_id'] = network[PROV_SEGMT_ID]
|
|
return network_info
|
|
|
|
def _get_vip_network_info(self, context, vip):
|
|
network_info = self._get_network_info(context, vip)
|
|
network_info['port_id'] = vip['port_id']
|
|
return network_info
|
|
|
|
def _get_pool_network_info(self, context, pool):
|
|
return self._get_network_info(context, pool)
|
|
|
|
def _get_pools_on_subnet(self, context, tenant_id, subnet_id):
|
|
filter_dict = {'subnet_id': [subnet_id], 'tenant_id': [tenant_id]}
|
|
return self.plugin.get_pools(context, filters=filter_dict)
|
|
|
|
def _get_snatport_for_subnet(self, context, tenant_id, subnet_id):
|
|
device_id = '_lb-snatport-' + subnet_id
|
|
subnet = self.plugin._core_plugin.get_subnet(context, subnet_id)
|
|
network_id = subnet['network_id']
|
|
msg = (_("Filtering ports based on network_id=%(network_id)s, "
|
|
"tenant_id=%(tenant_id)s, device_id=%(device_id)s") %
|
|
{'network_id': network_id,
|
|
'tenant_id': tenant_id,
|
|
'device_id': device_id})
|
|
LOG.debug(msg)
|
|
filter_dict = {
|
|
'network_id': [network_id],
|
|
'tenant_id': [tenant_id],
|
|
'device_id': [device_id],
|
|
'device-owner': [DRIVER_NAME]
|
|
}
|
|
ports = self.plugin._core_plugin.get_ports(context,
|
|
filters=filter_dict)
|
|
if ports:
|
|
msg = _("Found an existing SNAT port for subnet %s") % subnet_id
|
|
LOG.info(msg)
|
|
return ports[0]
|
|
msg = _("Found no SNAT ports for subnet %s") % subnet_id
|
|
LOG.info(msg)
|
|
|
|
def _create_snatport_for_subnet(self, context, tenant_id, subnet_id,
|
|
ip_address):
|
|
subnet = self.plugin._core_plugin.get_subnet(context, subnet_id)
|
|
fixed_ip = {'subnet_id': subnet['id']}
|
|
if ip_address and ip_address != attributes.ATTR_NOT_SPECIFIED:
|
|
fixed_ip['ip_address'] = ip_address
|
|
port_data = {
|
|
'tenant_id': tenant_id,
|
|
'name': '_lb-snatport-' + subnet_id,
|
|
'network_id': subnet['network_id'],
|
|
'mac_address': attributes.ATTR_NOT_SPECIFIED,
|
|
'admin_state_up': False,
|
|
'device_id': '_lb-snatport-' + subnet_id,
|
|
'device_owner': DRIVER_NAME,
|
|
'fixed_ips': [fixed_ip],
|
|
}
|
|
port = self.plugin._core_plugin.create_port(context,
|
|
{'port': port_data})
|
|
msg = _("Created SNAT port: %s") % repr(port)
|
|
LOG.info(msg)
|
|
return port
|
|
|
|
def _remove_snatport_for_subnet(self, context, tenant_id, subnet_id):
|
|
port = self._get_snatport_for_subnet(context, tenant_id, subnet_id)
|
|
if port:
|
|
self.plugin._core_plugin.delete_port(context, port['id'])
|
|
msg = _("Removed SNAT port: %s") % repr(port)
|
|
LOG.info(msg)
|
|
|
|
def _create_snatport_for_subnet_if_not_exists(self, context, tenant_id,
|
|
subnet_id, network_info):
|
|
port = self._get_snatport_for_subnet(context, tenant_id, subnet_id)
|
|
if not port:
|
|
msg = _("No SNAT port found for subnet %s."
|
|
" Creating one...") % subnet_id
|
|
LOG.info(msg)
|
|
port = self._create_snatport_for_subnet(context, tenant_id,
|
|
subnet_id,
|
|
ip_address=None)
|
|
network_info['port_id'] = port['id']
|
|
network_info['snat_ip'] = port['fixed_ips'][0]['ip_address']
|
|
msg = _("SNAT port: %s") % repr(port)
|
|
LOG.info(msg)
|
|
|
|
def _remove_snatport_for_subnet_if_not_used(self, context, tenant_id,
|
|
subnet_id):
|
|
pools = self._get_pools_on_subnet(context, tenant_id, subnet_id)
|
|
if not pools:
|
|
#No pools left on the old subnet.
|
|
#We can remove the SNAT port/ipaddress
|
|
self._remove_snatport_for_subnet(context, tenant_id, subnet_id)
|
|
msg = _("Removing SNAT port for subnet %s "
|
|
"as this is the last pool using it...") % subnet_id
|
|
LOG.info(msg)
|