Enable IPv6 load balancer networks

This patch addresses several places where IPv6 and IPv6 link-local
addresses where not considered for communication between amphora and the
controller worker.

In the devstack plugin we permit both IPv4 and IPv6 for health
monitoring and the amphora REST API.

In the amphora's UDP health sender we parse the IP port string in a
manner which permits IPv6 addresses by splitting on the last colon
rather than every colon.

In the controller REST API driver we append an interface scope if using
IPv6 link-local addresses. This interface can be specified by an
operator is they are using an interface other than o-hm0, this only is
required if using IPv6 link-local addresses.

Change-Id: I9d07bec4ac105e8876fadb72a83a590ffd4d2e66
This commit is contained in:
Dustin Lundquist 2016-10-27 06:51:53 -07:00
parent 8fef2f04a7
commit 6ce85349c9
9 changed files with 93 additions and 20 deletions

View File

@ -218,10 +218,14 @@ function build_mgmt_network {
openstack security group rule create --protocol icmp lb-mgmt-sec-grp
openstack security group rule create --protocol tcp --dst-port 22 lb-mgmt-sec-grp
openstack security group rule create --protocol tcp --dst-port 9443 lb-mgmt-sec-grp
openstack security group rule create --protocol icmpv6 --ethertype IPv6 --src-ip ::/0 lb-mgmt-sec-grp
openstack security group rule create --protocol tcp --dst-port 22 --ethertype IPv6 --src-ip ::/0 lb-mgmt-sec-grp
openstack security group rule create --protocol tcp --dst-port 9443 --ethertype IPv6 --src-ip ::/0 lb-mgmt-sec-grp
# Create security group and rules
openstack security group create lb-health-mgr-sec-grp
openstack security group rule create --protocol udp --dst-port $OCTAVIA_HM_LISTEN_PORT lb-health-mgr-sec-grp
openstack security group rule create --protocol udp --dst-port $OCTAVIA_HM_LISTEN_PORT --ethertype IPv6 --src-ip ::/0 lb-health-mgr-sec-grp
}
function configure_lb_mgmt_sec_grp {

View File

@ -112,6 +112,12 @@
# REST Driver specific
# bind_host = 0.0.0.0
# bind_port = 9443
#
# This setting is only needed with IPv6 link-local addresses (fe80::/64) are
# used for communication between Octavia and its Amphora, if IPv4 or other IPv6
# addresses are used it can be ignored.
# lb_network_interface = o-hm0
#
# haproxy_cmd = /usr/sbin/haproxy
# respawn_count = 2
# respawn_interval = 2
@ -236,7 +242,7 @@
# vrrp_garp_refresh_count = 2
[service_auth]
# memcached_servers =
# memcached_servers =
# signing_dir =
# cafile = /opt/stack/data/ca-bundle.pem
# project_domain_name = Default

View File

@ -37,8 +37,14 @@ class UDPStatusSender(object):
def __init__(self):
self.dests = []
for ipport in CONF.health_manager.controller_ip_port_list:
parts = ipport.split(':')
self.update(parts[0], parts[1])
try:
ip, port = ipport.rsplit(':', 1)
except ValueError:
LOG.error(_LE("Invalid ip and port '%s' in "
"health_manager controller_ip_port_list"),
ipport)
break
self.update(ip, port)
self.v4sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
self.v6sock = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
self.key = str(CONF.health_manager.heartbeat_key)

View File

@ -31,6 +31,7 @@ from octavia.common.config import cfg
from octavia.common import constants as consts
from octavia.common.jinja.haproxy import jinja_cfg
from octavia.common.tls_utils import cert_parser
from octavia.common import utils
from octavia.i18n import _LE, _LW
LOG = logging.getLogger(__name__)
@ -238,6 +239,12 @@ class AmphoraAPIClient(object):
self.session.mount('https://', self.ssl_adapter)
def _base_url(self, ip):
if utils.is_ipv6_lla(ip):
ip = '[{ip}%{interface}]'.format(
ip=ip,
interface=CONF.haproxy_amphora.lb_network_interface)
elif utils.is_ipv6(ip):
ip = '[{ip}]'.format(ip=ip)
return "https://{ip}:{port}/{version}/".format(
ip=ip,
port=CONF.haproxy_amphora.bind_port,

View File

@ -167,10 +167,14 @@ haproxy_amphora_opts = [
'suffixes. Example: 10k')),
# REST server
cfg.IPOpt('bind_host', default='0.0.0.0', # nosec
cfg.IPOpt('bind_host', default='::', # nosec
help=_("The host IP to bind to")),
cfg.PortOpt('bind_port', default=9443,
help=_("The port to bind to")),
cfg.StrOpt('lb_network_interface',
default='o-hm0',
help=_('Network interface through which to reach amphora, only '
'required if using IPv6 link local addresses.')),
cfg.StrOpt('haproxy_cmd', default='/usr/sbin/haproxy',
help=_("The full path to haproxy")),
cfg.IntOpt('respawn_count', default=2,

View File

@ -24,6 +24,7 @@ import hashlib
import random
import socket
import netaddr
from oslo_config import cfg
from oslo_log import log as logging
from oslo_utils import excutils
@ -70,6 +71,18 @@ def get_network_driver():
return network_driver
def is_ipv6(ip_address):
"""Check if ip address is IPv6 address."""
ip = netaddr.IPAddress(ip_address)
return ip.version == 6
def is_ipv6_lla(ip_address):
"""Check if ip address is IPv6 link local address."""
ip = netaddr.IPAddress(ip_address)
return ip.version == 6 and ip.is_link_local()
class exception_logger(object):
"""Wrap a function and log raised exception

View File

@ -24,7 +24,6 @@ from octavia.amphorae.backends.health_daemon import health_sender
from octavia.tests.unit import base
IP = '192.0.2.15'
IP_PORT = ['192.0.2.10:5555', '192.0.2.10:5555']
KEY = 'TEST'
PORT = random.randrange(1, 9000)
@ -38,7 +37,7 @@ class TestHealthSender(base.TestCase):
def setUp(self):
super(TestHealthSender, self).setUp()
self.conf = oslo_fixture.Config(cfg.CONF)
self.conf = self.useFixture(oslo_fixture.Config(cfg.CONF))
self.conf.config(group="health_manager",
controller_ip_port_list=IP_PORT)
self.conf.config(group="health_manager",
@ -53,66 +52,74 @@ class TestHealthSender(base.TestCase):
socket_mock.sendto = sendto_mock
# Test when no addresses are returned
mock_getaddrinfo.return_value = []
self.conf.config(group="health_manager",
controller_ip_port_list='')
sender = health_sender.UDPStatusSender()
sender.dosend(SAMPLE_MSG)
sendto_mock.reset_mock()
# Test IPv4 path
self.conf.config(group="health_manager",
controller_ip_port_list=['192.0.2.20:80'])
mock_getaddrinfo.return_value = [(socket.AF_INET,
socket.SOCK_DGRAM,
socket.IPPROTO_UDP,
'',
('192.0.2.20', 80))]
sendto_mock.reset_mock()
sender = health_sender.UDPStatusSender()
sender.dosend(SAMPLE_MSG)
sendto_mock.assert_called_once_with(SAMPLE_MSG_BIN,
('192.0.2.20', 80))
sendto_mock.reset_mock()
# Test IPv6 path
self.conf.config(group="health_manager",
controller_ip_port_list=['2001:0db8::f00d:80'])
mock_getaddrinfo.return_value = [(socket.AF_INET6,
socket.SOCK_DGRAM,
socket.IPPROTO_UDP,
'',
('2001:0DB8::F00D', 80))]
('2001:db8::f00d', 80, 0, 0))]
sender = health_sender.UDPStatusSender()
sender.dosend(SAMPLE_MSG)
sendto_mock.assert_called_once_with(SAMPLE_MSG_BIN,
('2001:0DB8::F00D', 80))
('2001:db8::f00d', 80, 0, 0))
sendto_mock.reset_mock()
# Test invalid address family
mock_getaddrinfo.return_value = [(socket.AF_UNIX,
# Test IPv6 link-local address path
self.conf.config(
group="health_manager",
controller_ip_port_list=['fe80::00ff:fe00:cafe%eth0:80'])
mock_getaddrinfo.return_value = [(socket.AF_INET6,
socket.SOCK_DGRAM,
socket.IPPROTO_UDP,
'',
('2001:0DB8::F00D', 80))]
('fe80::ff:fe00:cafe', 80, 0, 2))]
sender = health_sender.UDPStatusSender()
sender.dosend(SAMPLE_MSG)
self.assertFalse(sendto_mock.called)
sendto_mock.assert_called_once_with(SAMPLE_MSG_BIN,
('fe80::ff:fe00:cafe', 80, 0, 2))
sendto_mock.reset_mock()
# Test socket error
socket_mock.sendto.side_effect = socket.error
self.conf.config(group="health_manager",
controller_ip_port_list=['2001:0db8::f00d:80'])
mock_getaddrinfo.return_value = [(socket.AF_INET6,
socket.SOCK_DGRAM,
socket.IPPROTO_UDP,
'',
('2001:0DB8::F00D', 80))]
('2001:db8::f00d', 80, 0, 0))]
socket_mock.sendto.side_effect = socket.error
sender = health_sender.UDPStatusSender()

View File

@ -31,7 +31,9 @@ from octavia.tests.unit.common.sample_configs import sample_configs
FAKE_CIDR = '198.51.100.0/24'
FAKE_GATEWAY = '192.51.100.1'
FAKE_IP = 'fake'
FAKE_IP = '192.0.2.10'
FAKE_IPV6 = '2001:db8::cafe'
FAKE_IPV6_LLA = 'fe80::00ff:fe00:cafe'
FAKE_PEM_FILENAME = "file_name"
FAKE_UUID_1 = uuidutils.generate_uuid()
FAKE_VRRP_IP = '10.1.0.1'
@ -257,6 +259,14 @@ class TestAmphoraAPIClientTest(base.TestCase):
'mac_address': FAKE_MAC_ADDRESS,
'vrrp_ip': self.amp.vrrp_ip}
def test_base_url(self):
url = self.driver._base_url(FAKE_IP)
self.assertEqual('https://192.0.2.10:9443/0.5/', url)
url = self.driver._base_url(FAKE_IPV6)
self.assertEqual('https://[2001:db8::cafe]:9443/0.5/', url)
url = self.driver._base_url(FAKE_IPV6_LLA)
self.assertEqual('https://[fe80::00ff:fe00:cafe%o-hm0]:9443/0.5/', url)
@mock.patch('octavia.amphorae.drivers.haproxy.rest_api_driver.time.sleep')
def test_request(self, mock_sleep):
self.assertRaises(driver_except.TimeOutException,

View File

@ -23,3 +23,19 @@ class TestConfig(base.TestCase):
def test_random_string(self):
self.assertNotEqual(utils.get_random_string(10), '')
def test_is_ipv6(self):
self.assertFalse(utils.is_ipv6('192.0.2.10'))
self.assertFalse(utils.is_ipv6('169.254.0.10'))
self.assertFalse(utils.is_ipv6('0.0.0.0'))
self.assertTrue(utils.is_ipv6('::'))
self.assertTrue(utils.is_ipv6('2001:db8::1'))
self.assertTrue(utils.is_ipv6('fe80::225:90ff:fefb:53ad'))
def test_is_ipv6_lla(self):
self.assertFalse(utils.is_ipv6_lla('192.0.2.10'))
self.assertFalse(utils.is_ipv6_lla('169.254.0.10'))
self.assertFalse(utils.is_ipv6_lla('0.0.0.0'))
self.assertFalse(utils.is_ipv6_lla('::'))
self.assertFalse(utils.is_ipv6_lla('2001:db8::1'))
self.assertTrue(utils.is_ipv6_lla('fe80::225:90ff:fefb:53ad'))