Merge "Make L3 HA VIPs ordering consistent in keepalived.conf" into stable/juno
This commit is contained in:
commit
c0671644e4
|
@ -113,9 +113,11 @@ class AgentMixin(object):
|
|||
config = ri.keepalived_manager.config
|
||||
|
||||
interface_name = self.get_ha_device_name(ri.ha_port['id'])
|
||||
ha_port_cidr = ri.ha_port['subnet']['cidr']
|
||||
instance = keepalived.KeepalivedInstance(
|
||||
'BACKUP', interface_name, ri.ha_vr_id, nopreempt=True,
|
||||
advert_int=self.conf.ha_vrrp_advert_int, priority=ri.ha_priority)
|
||||
'BACKUP', interface_name, ri.ha_vr_id, ha_port_cidr,
|
||||
nopreempt=True, advert_int=self.conf.ha_vrrp_advert_int,
|
||||
priority=ri.ha_priority)
|
||||
instance.track_interfaces.append(interface_name)
|
||||
|
||||
if self.conf.ha_vrrp_auth_password:
|
||||
|
|
|
@ -16,6 +16,7 @@ import itertools
|
|||
import os
|
||||
import stat
|
||||
|
||||
import netaddr
|
||||
from oslo.config import cfg
|
||||
|
||||
from neutron.agent.linux import external_process
|
||||
|
@ -28,10 +29,35 @@ VALID_STATES = ['MASTER', 'BACKUP']
|
|||
VALID_NOTIFY_STATES = ['master', 'backup', 'fault']
|
||||
VALID_AUTH_TYPES = ['AH', 'PASS']
|
||||
HA_DEFAULT_PRIORITY = 50
|
||||
PRIMARY_VIP_RANGE_SIZE = 24
|
||||
# TODO(amuller): Use L3 agent constant when new constants module is introduced.
|
||||
FIP_LL_SUBNET = '169.254.30.0/23'
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_free_range(parent_range, excluded_ranges, size=PRIMARY_VIP_RANGE_SIZE):
|
||||
"""Get a free IP range, from parent_range, of the specified size.
|
||||
|
||||
:param parent_range: String representing an IP range. E.g: '169.254.0.0/16'
|
||||
:param excluded_ranges: A list of strings to be excluded from parent_range
|
||||
:param size: What should be the size of the range returned?
|
||||
:return: A string representing an IP range
|
||||
"""
|
||||
free_cidrs = netaddr.IPSet([parent_range]) - netaddr.IPSet(excluded_ranges)
|
||||
for cidr in free_cidrs.iter_cidrs():
|
||||
if cidr.prefixlen <= size:
|
||||
return '%s/%s' % (cidr.network, size)
|
||||
|
||||
raise ValueError(_('Network of size %(size)s, from IP range '
|
||||
'%(parent_range)s excluding IP ranges '
|
||||
'%(excluded_ranges)s was not found.') %
|
||||
{'size': size,
|
||||
'parent_range': parent_range,
|
||||
'excluded_ranges': excluded_ranges})
|
||||
|
||||
|
||||
class InvalidInstanceStateException(exceptions.NeutronException):
|
||||
message = (_('Invalid instance state: %%(state)s, valid states are: '
|
||||
'%(valid_states)s') %
|
||||
|
@ -106,7 +132,7 @@ class KeepalivedGroup(object):
|
|||
class KeepalivedInstance(object):
|
||||
"""Instance section of a keepalived configuration."""
|
||||
|
||||
def __init__(self, state, interface, vrouter_id,
|
||||
def __init__(self, state, interface, vrouter_id, ha_cidr,
|
||||
priority=HA_DEFAULT_PRIORITY, advert_int=None,
|
||||
mcast_src_ip=None, nopreempt=False):
|
||||
self.name = 'VR_%s' % vrouter_id
|
||||
|
@ -125,6 +151,13 @@ class KeepalivedInstance(object):
|
|||
self.vips = []
|
||||
self.virtual_routes = []
|
||||
self.authentication = tuple()
|
||||
metadata_cidr = '169.254.169.254/32'
|
||||
self.primary_vip_range = get_free_range(
|
||||
parent_range='169.254.0.0/16',
|
||||
excluded_ranges=[metadata_cidr,
|
||||
FIP_LL_SUBNET,
|
||||
ha_cidr],
|
||||
size=PRIMARY_VIP_RANGE_SIZE)
|
||||
|
||||
def set_authentication(self, auth_type, password):
|
||||
if auth_type not in VALID_AUTH_TYPES:
|
||||
|
@ -152,18 +185,46 @@ class KeepalivedInstance(object):
|
|||
(' %s' % i for i in self.track_interfaces),
|
||||
[' }'])
|
||||
|
||||
def _build_vips_config(self):
|
||||
vips_sorted = sorted(self.vips, key=lambda vip: vip.ip_address)
|
||||
first_address = vips_sorted.pop(0)
|
||||
def _generate_primary_vip(self):
|
||||
"""Return an address in the primary_vip_range CIDR, with the router's
|
||||
VRID in the host section.
|
||||
|
||||
For example, if primary_vip_range is 169.254.0.0/24, and this router's
|
||||
VRID is 5, the result is 169.254.0.5. Using the VRID assures that
|
||||
the primary VIP is consistent amongst HA router instances on different
|
||||
nodes.
|
||||
"""
|
||||
|
||||
ip = (netaddr.IPNetwork(self.primary_vip_range).network +
|
||||
self.vrouter_id)
|
||||
return netaddr.IPNetwork('%s/%s' % (ip, PRIMARY_VIP_RANGE_SIZE))
|
||||
|
||||
def _build_vips_config(self):
|
||||
# NOTE(amuller): The primary VIP must be consistent in order to avoid
|
||||
# keepalived bugs. Changing the VIP in the 'virtual_ipaddress' and
|
||||
# SIGHUP'ing keepalived can remove virtual routers, including the
|
||||
# router's default gateway.
|
||||
# We solve this by never changing the VIP in the virtual_ipaddress
|
||||
# section, herein known as the primary VIP.
|
||||
# The only interface known to exist for HA routers is the HA interface
|
||||
# (self.interface). We generate an IP on that device and use it as the
|
||||
# primary VIP. The other VIPs (Internal interfaces IPs, the external
|
||||
# interface IP and floating IPs) are placed in the
|
||||
# virtual_ipaddress_excluded section.
|
||||
|
||||
primary = KeepalivedVipAddress(str(self._generate_primary_vip()),
|
||||
self.interface)
|
||||
vips_result = [' virtual_ipaddress {',
|
||||
' %s' % first_address.build_config(),
|
||||
' %s' % primary.build_config(),
|
||||
' }']
|
||||
if vips_sorted:
|
||||
|
||||
if self.vips:
|
||||
vips_result.extend(
|
||||
itertools.chain([' virtual_ipaddress_excluded {'],
|
||||
(' %s' % vip.build_config()
|
||||
for vip in vips_sorted),
|
||||
for vip in
|
||||
sorted(self.vips,
|
||||
key=lambda vip: vip.ip_address)),
|
||||
[' }']))
|
||||
|
||||
return vips_result
|
||||
|
@ -201,8 +262,7 @@ class KeepalivedInstance(object):
|
|||
if self.track_interfaces:
|
||||
config.extend(self._build_track_interface_config())
|
||||
|
||||
if self.vips:
|
||||
config.extend(self._build_vips_config())
|
||||
config.extend(self._build_vips_config())
|
||||
|
||||
if self.virtual_routes:
|
||||
config.extend(self._build_virtual_routes_config())
|
||||
|
|
|
@ -144,9 +144,10 @@ vrrp_instance VR_1 {
|
|||
%(ha_device_name)s
|
||||
}
|
||||
virtual_ipaddress {
|
||||
%(floating_ip_cidr)s dev %(external_device_name)s
|
||||
169.254.0.1/24 dev %(ha_device_name)s
|
||||
}
|
||||
virtual_ipaddress_excluded {
|
||||
%(floating_ip_cidr)s dev %(external_device_name)s
|
||||
%(external_device_cidr)s dev %(external_device_name)s
|
||||
%(internal_device_cidr)s dev %(internal_device_name)s
|
||||
}
|
||||
|
|
|
@ -12,6 +12,8 @@
|
|||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import testtools
|
||||
|
||||
from neutron.agent.linux import keepalived
|
||||
from neutron.tests import base
|
||||
|
||||
|
@ -19,6 +21,40 @@ from neutron.tests import base
|
|||
# http://www.keepalived.org/pdf/UserGuide.pdf
|
||||
|
||||
|
||||
class KeepalivedGetFreeRangeTestCase(base.BaseTestCase):
|
||||
def test_get_free_range(self):
|
||||
free_range = keepalived.get_free_range(
|
||||
parent_range='169.254.0.0/16',
|
||||
excluded_ranges=['169.254.0.0/24',
|
||||
'169.254.1.0/24',
|
||||
'169.254.2.0/24'],
|
||||
size=24)
|
||||
self.assertEqual('169.254.3.0/24', free_range)
|
||||
|
||||
def test_get_free_range_without_excluded(self):
|
||||
free_range = keepalived.get_free_range(
|
||||
parent_range='169.254.0.0/16',
|
||||
excluded_ranges=[],
|
||||
size=20)
|
||||
self.assertEqual('169.254.0.0/20', free_range)
|
||||
|
||||
def test_get_free_range_excluded_out_of_parent(self):
|
||||
free_range = keepalived.get_free_range(
|
||||
parent_range='169.254.0.0/16',
|
||||
excluded_ranges=['255.255.255.0/24'],
|
||||
size=24)
|
||||
self.assertEqual('169.254.0.0/24', free_range)
|
||||
|
||||
def test_get_free_range_not_found(self):
|
||||
tiny_parent_range = '192.168.1.0/24'
|
||||
huge_size = 8
|
||||
with testtools.ExpectedException(ValueError):
|
||||
keepalived.get_free_range(
|
||||
parent_range=tiny_parent_range,
|
||||
excluded_ranges=[],
|
||||
size=huge_size)
|
||||
|
||||
|
||||
class KeepalivedConfBaseMixin(object):
|
||||
|
||||
def _get_config(self):
|
||||
|
@ -30,6 +66,7 @@ class KeepalivedConfBaseMixin(object):
|
|||
group1.set_notify('master', '/tmp/script.sh')
|
||||
|
||||
instance1 = keepalived.KeepalivedInstance('MASTER', 'eth0', 1,
|
||||
'169.254.192.0/18',
|
||||
advert_int=5)
|
||||
instance1.set_authentication('AH', 'pass123')
|
||||
instance1.track_interfaces.append("eth0")
|
||||
|
@ -59,6 +96,7 @@ class KeepalivedConfBaseMixin(object):
|
|||
group1.add_instance(instance1)
|
||||
|
||||
instance2 = keepalived.KeepalivedInstance('MASTER', 'eth4', 2,
|
||||
'169.254.192.0/18',
|
||||
mcast_src_ip='224.0.0.1')
|
||||
instance2.track_interfaces.append("eth4")
|
||||
|
||||
|
@ -107,9 +145,10 @@ vrrp_instance VR_1 {
|
|||
eth0
|
||||
}
|
||||
virtual_ipaddress {
|
||||
192.168.1.0/24 dev eth1
|
||||
169.254.0.1/24 dev eth0
|
||||
}
|
||||
virtual_ipaddress_excluded {
|
||||
192.168.1.0/24 dev eth1
|
||||
192.168.2.0/24 dev eth2
|
||||
192.168.3.0/24 dev eth2
|
||||
192.168.55.0/24 dev eth10
|
||||
|
@ -128,9 +167,10 @@ vrrp_instance VR_2 {
|
|||
eth4
|
||||
}
|
||||
virtual_ipaddress {
|
||||
192.168.2.0/24 dev eth2
|
||||
169.254.0.2/24 dev eth4
|
||||
}
|
||||
virtual_ipaddress_excluded {
|
||||
192.168.2.0/24 dev eth2
|
||||
192.168.3.0/24 dev eth6
|
||||
192.168.55.0/24 dev eth10
|
||||
}
|
||||
|
@ -160,10 +200,11 @@ class KeepalivedStateExceptionTestCase(base.BaseTestCase):
|
|||
invalid_vrrp_state = 'into a club'
|
||||
self.assertRaises(keepalived.InvalidInstanceStateException,
|
||||
keepalived.KeepalivedInstance,
|
||||
invalid_vrrp_state, 'eth0', 33)
|
||||
invalid_vrrp_state, 'eth0', 33, '169.254.192.0/18')
|
||||
|
||||
invalid_auth_type = '[hip, hip]'
|
||||
instance = keepalived.KeepalivedInstance('MASTER', 'eth0', 1)
|
||||
instance = keepalived.KeepalivedInstance('MASTER', 'eth0', 1,
|
||||
'169.254.192.0/18')
|
||||
self.assertRaises(keepalived.InvalidAuthenticationTypeExecption,
|
||||
instance.set_authentication,
|
||||
invalid_auth_type, 'some_password')
|
||||
|
@ -171,6 +212,12 @@ class KeepalivedStateExceptionTestCase(base.BaseTestCase):
|
|||
|
||||
class KeepalivedInstanceTestCase(base.BaseTestCase,
|
||||
KeepalivedConfBaseMixin):
|
||||
def test_generate_primary_vip(self):
|
||||
instance = keepalived.KeepalivedInstance('MASTER', 'ha0', 42,
|
||||
'169.254.192.0/18')
|
||||
self.assertEqual('169.254.0.42/24',
|
||||
str(instance._generate_primary_vip()))
|
||||
|
||||
def test_remove_adresses_by_interface(self):
|
||||
config = self._get_config()
|
||||
instance = config.get_instance(1)
|
||||
|
@ -202,6 +249,9 @@ vrrp_instance VR_1 {
|
|||
eth0
|
||||
}
|
||||
virtual_ipaddress {
|
||||
169.254.0.1/24 dev eth0
|
||||
}
|
||||
virtual_ipaddress_excluded {
|
||||
192.168.1.0/24 dev eth1
|
||||
}
|
||||
virtual_routes {
|
||||
|
@ -218,9 +268,10 @@ vrrp_instance VR_2 {
|
|||
eth4
|
||||
}
|
||||
virtual_ipaddress {
|
||||
192.168.2.0/24 dev eth2
|
||||
169.254.0.2/24 dev eth4
|
||||
}
|
||||
virtual_ipaddress_excluded {
|
||||
192.168.2.0/24 dev eth2
|
||||
192.168.3.0/24 dev eth6
|
||||
192.168.55.0/24 dev eth10
|
||||
}
|
||||
|
@ -228,6 +279,20 @@ vrrp_instance VR_2 {
|
|||
|
||||
self.assertEqual(expected, config.get_config_str())
|
||||
|
||||
def test_build_config_no_vips(self):
|
||||
expected = """vrrp_instance VR_1 {
|
||||
state MASTER
|
||||
interface eth0
|
||||
virtual_router_id 1
|
||||
priority 50
|
||||
virtual_ipaddress {
|
||||
169.254.0.1/24 dev eth0
|
||||
}
|
||||
}"""
|
||||
instance = keepalived.KeepalivedInstance(
|
||||
'MASTER', 'eth0', 1, '169.254.192.0/18')
|
||||
self.assertEqual(expected, '\n'.join(instance.build_config()))
|
||||
|
||||
|
||||
class KeepalivedVirtualRouteTestCase(base.BaseTestCase):
|
||||
def test_virtual_route_with_dev(self):
|
||||
|
|
|
@ -301,8 +301,8 @@ def get_ha_interface():
|
|||
'name': u'L3 HA Admin port 0',
|
||||
'network_id': _uuid(),
|
||||
'status': u'ACTIVE',
|
||||
'subnet': {'cidr': '169.254.0.0/24',
|
||||
'gateway_ip': '169.254.0.1',
|
||||
'subnet': {'cidr': '169.254.192.0/18',
|
||||
'gateway_ip': '169.254.255.254',
|
||||
'id': _uuid()},
|
||||
'tenant_id': '',
|
||||
'agent_id': _uuid(),
|
||||
|
@ -1058,60 +1058,6 @@ class TestBasicRouterOperations(base.BaseTestCase):
|
|||
self.assertEqual(agent.process_router_floating_ip_nat_rules.called,
|
||||
distributed)
|
||||
|
||||
def test_ha_router_keepalived_config(self):
|
||||
agent = l3_agent.L3NATAgent(HOSTNAME, self.conf)
|
||||
router = prepare_router_data(enable_ha=True)
|
||||
router['routes'] = [
|
||||
{'destination': '8.8.8.8/32', 'nexthop': '35.4.0.10'},
|
||||
{'destination': '8.8.4.4/32', 'nexthop': '35.4.0.11'}]
|
||||
ri = l3_agent.RouterInfo(router['id'], self.conf.root_helper,
|
||||
self.conf.use_namespaces, router=router)
|
||||
ri.router = router
|
||||
with contextlib.nested(mock.patch.object(agent,
|
||||
'_spawn_metadata_proxy'),
|
||||
mock.patch('neutron.agent.linux.'
|
||||
'utils.replace_file'),
|
||||
mock.patch('neutron.agent.linux.'
|
||||
'utils.execute'),
|
||||
mock.patch('os.makedirs')):
|
||||
agent.process_ha_router_added(ri)
|
||||
agent.process_router(ri)
|
||||
config = ri.keepalived_manager.config
|
||||
ha_iface = agent.get_ha_device_name(ri.ha_port['id'])
|
||||
ex_iface = agent.get_external_device_name(ri.ex_gw_port['id'])
|
||||
int_iface = agent.get_internal_device_name(
|
||||
ri.internal_ports[0]['id'])
|
||||
|
||||
expected = """vrrp_sync_group VG_1 {
|
||||
group {
|
||||
VR_1
|
||||
}
|
||||
}
|
||||
vrrp_instance VR_1 {
|
||||
state BACKUP
|
||||
interface %(ha_iface)s
|
||||
virtual_router_id 1
|
||||
priority 50
|
||||
nopreempt
|
||||
advert_int 2
|
||||
track_interface {
|
||||
%(ha_iface)s
|
||||
}
|
||||
virtual_ipaddress {
|
||||
19.4.4.4/24 dev %(ex_iface)s
|
||||
}
|
||||
virtual_ipaddress_excluded {
|
||||
35.4.0.4/24 dev %(int_iface)s
|
||||
}
|
||||
virtual_routes {
|
||||
0.0.0.0/0 via 19.4.4.1 dev %(ex_iface)s
|
||||
8.8.8.8/32 via 35.4.0.10
|
||||
8.8.4.4/32 via 35.4.0.11
|
||||
}
|
||||
}""" % {'ha_iface': ha_iface, 'ex_iface': ex_iface, 'int_iface': int_iface}
|
||||
|
||||
self.assertEqual(expected, config.get_config_str())
|
||||
|
||||
@mock.patch('neutron.agent.linux.ip_lib.IPDevice')
|
||||
def _test_process_router_floating_ip_addresses_add(self, ri,
|
||||
agent, IPDevice):
|
||||
|
|
Loading…
Reference in New Issue