vmware-nsx/vmware_nsx/services/lbaas/nsx_v/lbaas_common.py
Kobi Samoray c662acf6e0 LBaaS legacy mode bugfix
In LBaaS legacy mode we use the exclusive router edge as platform for
neutron LBaaS.

The following patch addresses two issues in this mode:
- In the legacy code, LBaaS driver maintained a DFW section which allows
 traffic between the LB and its members. This code wasn't added when we
 added the legacy mode.
- The original code had a bug which failed to cleanup these DFW rules when
 a pool or members were deleted. The patch adds an admin utility which
 cleans up such stale DFW rules.

Change-Id: I1c95ec6292e6cf50641581a65cbb4bdf8942aa8f
2018-06-14 14:21:10 +03:00

393 lines
14 KiB
Python

# Copyright 2015 VMware, Inc.
# All Rights Reserved
#
# 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 xml.etree.ElementTree as et
import netaddr
from neutron_lib import constants
from neutron_lib import exceptions as n_exc
from oslo_log import log as logging
from vmware_nsx._i18n import _
from vmware_nsx.common import locking
from vmware_nsx.db import nsxv_db
from vmware_nsx.plugins.nsx_v.vshield import edge_utils
from vmware_nsx.plugins.nsx_v.vshield import vcns as nsxv_api
LOG = logging.getLogger(__name__)
MEMBER_ID_PFX = 'member-'
RESOURCE_ID_PFX = 'lbaas-'
LBAAS_FW_SECTION_NAME = 'LBaaS FW Rules'
def get_member_id(member_id):
return MEMBER_ID_PFX + member_id
def get_lb_resource_id(lb_id):
return (RESOURCE_ID_PFX + lb_id)[:36]
def get_lbaas_edge_id_for_subnet(context, plugin, subnet_id, tenant_id):
"""
Grab the id of an Edge appliance that is connected to subnet_id.
"""
subnet = plugin.get_subnet(context, subnet_id)
net_id = subnet.get('network_id')
filters = {'network_id': [net_id],
'device_owner': ['network:router_interface'],
'tenant_id': [tenant_id]}
attached_routers = plugin.get_ports(context.elevated(),
filters=filters,
fields=['device_id'])
for attached_router in attached_routers:
router = plugin.get_router(context, attached_router['device_id'])
if router.get('router_type') == 'exclusive':
rtr_bindings = nsxv_db.get_nsxv_router_binding(context.session,
router['id'])
return rtr_bindings['edge_id']
def get_lb_edge_name(context, lb_id):
"""Look for the resource name of the edge hosting the LB.
For older loadbalancers this may be a router edge
"""
binding = nsxv_db.get_nsxv_lbaas_loadbalancer_binding(
context.session, lb_id)
if binding:
edge_binding = nsxv_db.get_nsxv_router_binding_by_edge(
context.session, binding['edge_id'])
if edge_binding:
return edge_binding['router_id']
# fallback
return get_lb_resource_id(lb_id)
def get_lb_interface(context, plugin, lb_id, subnet_id):
filters = {'fixed_ips': {'subnet_id': [subnet_id]},
'device_id': [lb_id],
'device_owner': [constants.DEVICE_OWNER_NEUTRON_PREFIX + 'LB']}
lb_ports = plugin.get_ports(context.elevated(), filters=filters)
return lb_ports
def create_lb_interface(context, plugin, lb_id, subnet_id, tenant_id,
vip_addr=None, subnet=None):
if not subnet:
subnet = plugin.get_subnet(context, subnet_id)
network_id = subnet.get('network_id')
port_dict = {'name': 'lb_if-' + lb_id,
'admin_state_up': True,
'network_id': network_id,
'tenant_id': tenant_id,
'fixed_ips': [{'subnet_id': subnet['id']}],
'device_owner': constants.DEVICE_OWNER_NEUTRON_PREFIX + 'LB',
'device_id': lb_id,
'mac_address': constants.ATTR_NOT_SPECIFIED
}
port = plugin.base_create_port(context, {'port': port_dict})
ip_addr = port['fixed_ips'][0]['ip_address']
net = netaddr.IPNetwork(subnet['cidr'])
resource_id = get_lb_edge_name(context, lb_id)
address_groups = [{'primaryAddress': ip_addr,
'subnetPrefixLength': str(net.prefixlen),
'subnetMask': str(net.netmask)}]
if vip_addr:
address_groups[0]['secondaryAddresses'] = {
'type': 'secondary_addresses', 'ipAddress': [vip_addr]}
edge_utils.update_internal_interface(
plugin.nsx_v, context, resource_id,
network_id, address_groups)
def delete_lb_interface(context, plugin, lb_id, subnet_id):
resource_id = get_lb_edge_name(context, lb_id)
subnet = plugin.get_subnet(context, subnet_id)
network_id = subnet.get('network_id')
lb_ports = get_lb_interface(context, plugin, lb_id, subnet_id)
for lb_port in lb_ports:
plugin.delete_port(context, lb_port['id'])
edge_utils.delete_interface(plugin.nsx_v, context, resource_id, network_id,
dist=False)
def get_lbaas_edge_id(context, plugin, lb_id, vip_addr, subnet_id, tenant_id,
appliance_size):
subnet = plugin.get_subnet(context, subnet_id)
network_id = subnet.get('network_id')
availability_zone = plugin.get_network_az_by_net_id(context, network_id)
resource_id = get_lb_resource_id(lb_id)
edge_id = plugin.edge_manager.allocate_lb_edge_appliance(
context, resource_id, availability_zone=availability_zone,
appliance_size=appliance_size)
create_lb_interface(context, plugin, lb_id, subnet_id, tenant_id,
vip_addr=vip_addr, subnet=subnet)
gw_ip = subnet.get('gateway_ip')
if gw_ip or subnet['host_routes']:
routes = [{'cidr': r['destination'],
'nexthop': r['nexthop']} for r in
subnet['host_routes']]
plugin.nsx_v.update_routes(edge_id, gw_ip, routes)
return edge_id
def find_address_in_same_subnet(ip_addr, address_groups):
"""
Lookup an address group with a matching subnet to ip_addr.
If found, return address_group.
"""
for address_group in address_groups['addressGroups']:
net_addr = '%(primaryAddress)s/%(subnetPrefixLength)s' % address_group
if netaddr.IPAddress(ip_addr) in netaddr.IPNetwork(net_addr):
return address_group
def add_address_to_address_groups(ip_addr, address_groups):
"""
Add ip_addr as a secondary IP address to an address group which belongs to
the same subnet.
"""
address_group = find_address_in_same_subnet(
ip_addr, address_groups)
if address_group:
sec_addr = address_group.get('secondaryAddresses')
if not sec_addr:
sec_addr = {
'type': 'secondary_addresses',
'ipAddress': [ip_addr]}
else:
sec_addr['ipAddress'].append(ip_addr)
address_group['secondaryAddresses'] = sec_addr
return True
return False
def del_address_from_address_groups(ip_addr, address_groups):
"""
Delete ip_addr from secondary address list in address groups.
"""
address_group = find_address_in_same_subnet(ip_addr, address_groups)
if address_group:
sec_addr = address_group.get('secondaryAddresses')
if sec_addr and ip_addr in sec_addr['ipAddress']:
sec_addr['ipAddress'].remove(ip_addr)
return True
return False
def vip_as_secondary_ip(vcns, edge_id, vip, handler):
with locking.LockManager.get_lock(edge_id):
r = vcns.get_interfaces(edge_id)[1]
vnics = r.get('vnics', [])
for vnic in vnics:
if vnic['type'] == 'trunk':
for sub_interface in vnic.get('subInterfaces', {}).get(
'subInterfaces', []):
address_groups = sub_interface.get('addressGroups')
if handler(vip, address_groups):
vcns.update_interface(edge_id, vnic)
return True
else:
address_groups = vnic.get('addressGroups')
if handler(vip, address_groups):
vcns.update_interface(edge_id, vnic)
return True
return False
def add_vip_as_secondary_ip(vcns, edge_id, vip):
"""
Edge appliance requires that a VIP will be configured as a primary
or a secondary IP address on an interface.
To do so, we locate an interface which is connected to the same subnet
that vip belongs to.
This can be a regular interface, on a sub-interface on a trunk.
"""
if not vip_as_secondary_ip(vcns, edge_id, vip,
add_address_to_address_groups):
msg = _('Failed to add VIP %(vip)s as secondary IP on '
'Edge %(edge_id)s') % {'vip': vip, 'edge_id': edge_id}
raise n_exc.BadRequest(resource='edge-lbaas', msg=msg)
def del_vip_as_secondary_ip(vcns, edge_id, vip):
"""
While removing vip, delete the secondary interface from Edge config.
"""
if not vip_as_secondary_ip(vcns, edge_id, vip,
del_address_from_address_groups):
msg = _('Failed to delete VIP %(vip)s as secondary IP on '
'Edge %(edge_id)s') % {'vip': vip, 'edge_id': edge_id}
raise n_exc.BadRequest(resource='edge-lbaas', msg=msg)
def extract_resource_id(location_uri):
"""
Edge assigns an ID for each resource that is being created:
it is postfixes the uri specified in the Location header.
This ID should be used while updating/deleting this resource.
"""
uri_elements = location_uri.split('/')
return uri_elements[-1]
def set_lb_firewall_default_rule(vcns, edge_id, action):
with locking.LockManager.get_lock(edge_id):
vcns.update_firewall_default_policy(edge_id, {'action': action})
def add_vip_fw_rule(vcns, edge_id, vip_id, ip_address):
fw_rule = {
'firewallRules': [
{'action': 'accept', 'destination': {
'ipAddress': [ip_address]},
'enabled': True,
'name': vip_id}]}
with locking.LockManager.get_lock(edge_id):
h = vcns.add_firewall_rule(edge_id, fw_rule)[0]
fw_rule_id = extract_resource_id(h['location'])
return fw_rule_id
def del_vip_fw_rule(vcns, edge_id, vip_fw_rule_id):
with locking.LockManager.get_lock(edge_id):
vcns.delete_firewall_rule(edge_id, vip_fw_rule_id)
def get_edge_ip_addresses(vcns, edge_id):
edge_ips = []
r = vcns.get_interfaces(edge_id)[1]
vnics = r.get('vnics', [])
for vnic in vnics:
if vnic['type'] == 'trunk':
for sub_interface in vnic.get('subInterfaces', {}).get(
'subInterfaces', []):
address_groups = sub_interface.get('addressGroups')
for address_group in address_groups['addressGroups']:
edge_ips.append(address_group['primaryAddress'])
else:
address_groups = vnic.get('addressGroups')
for address_group in address_groups['addressGroups']:
edge_ips.append(address_group['primaryAddress'])
return edge_ips
def update_pool_fw_rule(vcns, pool_id, edge_id, section_id, member_ips):
edge_ips = get_edge_ip_addresses(vcns, edge_id)
with locking.LockManager.get_lock('lbaas-fw-section'):
section_uri = '%s/%s/%s' % (nsxv_api.FIREWALL_PREFIX,
'layer3sections',
section_id)
xml_section = vcns.get_section(section_uri)[1]
section = et.fromstring(xml_section)
pool_rule = None
for rule in section.iter('rule'):
if rule.find('name').text == pool_id:
pool_rule = rule
if member_ips:
pool_rule.find('sources').find('source').find(
'value').text = (','.join(edge_ips))
pool_rule.find('destinations').find(
'destination').find('value').text = ','.join(
member_ips)
else:
section.remove(pool_rule)
break
if member_ips and pool_rule is None:
pool_rule = et.SubElement(section, 'rule')
et.SubElement(pool_rule, 'name').text = pool_id
et.SubElement(pool_rule, 'action').text = 'allow'
sources = et.SubElement(pool_rule, 'sources')
sources.attrib['excluded'] = 'false'
source = et.SubElement(sources, 'source')
et.SubElement(source, 'type').text = 'Ipv4Address'
et.SubElement(source, 'value').text = ','.join(edge_ips)
destinations = et.SubElement(pool_rule, 'destinations')
destinations.attrib['excluded'] = 'false'
destination = et.SubElement(destinations, 'destination')
et.SubElement(destination, 'type').text = 'Ipv4Address'
et.SubElement(destination, 'value').text = ','.join(member_ips)
vcns.update_section(section_uri,
et.tostring(section, encoding="us-ascii"),
None)
def get_lbaas_fw_section_id(vcns):
# Avoid concurrent creation of section by multiple neutron
# instances
with locking.LockManager.get_lock('lbaas-fw-section'):
fw_section_id = vcns.get_section_id(LBAAS_FW_SECTION_NAME)
if not fw_section_id:
section = et.Element('section')
section.attrib['name'] = LBAAS_FW_SECTION_NAME
sect = vcns.create_section('ip', et.tostring(section))[1]
fw_section_id = et.fromstring(sect).attrib['id']
return fw_section_id
def enable_edge_acceleration(vcns, edge_id):
with locking.LockManager.get_lock(edge_id):
# Query the existing load balancer config in case metadata lb is set
_, config = vcns.get_loadbalancer_config(edge_id)
config['accelerationEnabled'] = True
config['enabled'] = True
config['featureType'] = 'loadbalancer_4.0'
vcns.enable_service_loadbalancer(edge_id, config)
def is_lb_on_router_edge(context, core_plugin, edge_id):
binding = nsxv_db.get_nsxv_router_binding_by_edge(
context.session, edge_id)
router_id = binding['router_id']
if router_id.startswith(RESOURCE_ID_PFX):
# New lbaas edge
return False
# verify that this is a router (and an exclusive one)
try:
router = core_plugin.get_router(context, router_id)
if router.get('router_type') == 'exclusive':
return True
except Exception:
pass
LOG.error("Edge %(edge)s router %(rtr)s is not an lbaas edge, but also "
"not an exclusive router",
{'edge': edge_id, 'rtr': router_id})
return False