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:
parent
8fef2f04a7
commit
6ce85349c9
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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'))
|
||||
|
|
Loading…
Reference in New Issue