Fix incorrect removal of IP rules in the amphora

When plugging a member, an IP rule was deleted from the amphora. It
triggered connectivity issue when a client on subnet1 sends a request to
the VIP (on subnet2) while subnet1 is also plugged to the amp as a
member subnet.
The IP rules should be updated only when plugging/updating the VIP
interface. The JSON files that store the configuration of the interfaces
now contain the type of the interface ('vip', 'lo' or 'member') and the
rules update step is skipped when the type is not 'vip'.

Closes-Bug: #2016475

Change-Id: I3fe9bb584c8522482e9d7289ae8ea1aab205dd4d
This commit is contained in:
Gregory Thiemonge 2023-04-26 16:55:04 -04:00
parent e2bc07222f
commit b3bf177992
9 changed files with 117 additions and 12 deletions

View File

@ -56,6 +56,7 @@ class BaseOS(object):
def write_interface_file(self, interface, ip_address, prefixlen): def write_interface_file(self, interface, ip_address, prefixlen):
interface = interface_file.InterfaceFile( interface = interface_file.InterfaceFile(
name=interface, name=interface,
if_type=consts.LO,
addresses=[{ addresses=[{
"address": ip_address, "address": ip_address,
"prefixlen": prefixlen "prefixlen": prefixlen

View File

@ -197,6 +197,8 @@ class InterfaceController(object):
self._addresses_up(interface, ipr, idx) self._addresses_up(interface, ipr, idx)
self._routes_up(interface, ipr, idx) self._routes_up(interface, ipr, idx)
# only the vip port updates the rules
if interface.if_type == consts.VIP:
self._rules_up(interface, ipr, idx) self._rules_up(interface, ipr, idx)
self._scripts_up(interface, current_state) self._scripts_up(interface, current_state)
@ -378,6 +380,8 @@ class InterfaceController(object):
current_state = link.get(consts.STATE) current_state = link.get(consts.STATE)
if current_state == consts.IFACE_UP: if current_state == consts.IFACE_UP:
# only the vip port updates the rules
if interface.if_type == consts.VIP:
for rule in interface.rules: for rule in interface.rules:
rule[consts.FAMILY] = self._family(rule[consts.SRC]) rule[consts.FAMILY] = self._family(rule[consts.SRC])
LOG.debug("%s: Deleting rule %s", interface.name, rule) LOG.debug("%s: Deleting rule %s", interface.name, rule)

View File

@ -25,9 +25,11 @@ CONF = cfg.CONF
class InterfaceFile(object): class InterfaceFile(object):
def __init__(self, name, mtu=None, addresses=None, def __init__(self, name, if_type,
mtu=None, addresses=None,
routes=None, rules=None, scripts=None): routes=None, rules=None, scripts=None):
self.name = name self.name = name
self.if_type = if_type
self.mtu = mtu self.mtu = mtu
self.addresses = addresses or [] self.addresses = addresses or []
self.routes = routes or [] self.routes = routes or []
@ -92,6 +94,7 @@ class InterfaceFile(object):
flags, mode), 'w') as fp: flags, mode), 'w') as fp:
interface = { interface = {
consts.NAME: self.name, consts.NAME: self.name,
consts.IF_TYPE: self.if_type,
consts.ADDRESSES: self.addresses, consts.ADDRESSES: self.addresses,
consts.ROUTES: self.routes, consts.ROUTES: self.routes,
consts.RULES: self.rules, consts.RULES: self.rules,
@ -105,7 +108,7 @@ class InterfaceFile(object):
class VIPInterfaceFile(InterfaceFile): class VIPInterfaceFile(InterfaceFile):
def __init__(self, name, mtu, vips, vrrp_info, fixed_ips, topology): def __init__(self, name, mtu, vips, vrrp_info, fixed_ips, topology):
super().__init__(name, mtu=mtu) super().__init__(name, if_type=consts.VIP, mtu=mtu)
has_ipv4 = any(vip['ip_version'] == 4 for vip in vips) has_ipv4 = any(vip['ip_version'] == 4 for vip in vips)
has_ipv6 = any(vip['ip_version'] == 6 for vip in vips) has_ipv6 = any(vip['ip_version'] == 6 for vip in vips)
@ -236,7 +239,7 @@ class VIPInterfaceFile(InterfaceFile):
class PortInterfaceFile(InterfaceFile): class PortInterfaceFile(InterfaceFile):
def __init__(self, name, mtu, fixed_ips): def __init__(self, name, mtu, fixed_ips):
super().__init__(name, mtu=mtu) super().__init__(name, if_type=consts.BACKEND, mtu=mtu)
if fixed_ips: if fixed_ips:
ip_versions = set() ip_versions = set()

View File

@ -911,6 +911,9 @@ AMPHORA_SUPPORTED_ALPN_PROTOCOLS = [lib_consts.ALPN_PROTOCOL_HTTP_2,
lib_consts.ALPN_PROTOCOL_HTTP_1_0] lib_consts.ALPN_PROTOCOL_HTTP_1_0]
# Amphora interface fields # Amphora interface fields
IF_TYPE = 'if_type'
BACKEND = 'backend'
LO = 'lo'
MTU = 'mtu' MTU = 'mtu'
ADDRESSES = 'addresses' ADDRESSES = 'addresses'
ROUTES = 'routes' ROUTES = 'routes'

View File

@ -106,7 +106,7 @@ class TestOSUtils(base.TestCase):
'192.0.2.2', 16) '192.0.2.2', 16)
mock_interface_file.assert_called_once_with( mock_interface_file.assert_called_once_with(
name='eth1', name='eth1', if_type="lo",
addresses=[{"address": "192.0.2.2", "prefixlen": 16}]) addresses=[{"address": "192.0.2.2", "prefixlen": 16}])
mock_interface.write.assert_called_once() mock_interface.write.assert_called_once()

View File

@ -74,6 +74,7 @@ class TestInterface(base.TestCase):
'],\n' '],\n'
'"mtu": 1450,\n' '"mtu": 1450,\n'
'"name": "eth1",\n' '"name": "eth1",\n'
'"if_type": "mytype",\n'
'"routes": [\n' '"routes": [\n'
'{"dst": "0.0.0.0/0",\n' '{"dst": "0.0.0.0/0",\n'
'"gateway": "10.0.0.1"},\n' '"gateway": "10.0.0.1"},\n'
@ -107,6 +108,7 @@ class TestInterface(base.TestCase):
expected_dict = { expected_dict = {
consts.NAME: "eth1", consts.NAME: "eth1",
consts.IF_TYPE: "mytype",
consts.MTU: 1450, consts.MTU: 1450,
consts.ADDRESSES: [{ consts.ADDRESSES: [{
consts.ADDRESS: "10.0.0.2", consts.ADDRESS: "10.0.0.2",
@ -331,6 +333,7 @@ class TestInterface(base.TestCase):
mock_link, mock_addr, mock_route, mock_rule): mock_link, mock_addr, mock_route, mock_rule):
iface = interface_file.InterfaceFile( iface = interface_file.InterfaceFile(
name="eth1", name="eth1",
if_type="vip",
mtu=1450, mtu=1450,
addresses=[{ addresses=[{
consts.ADDRESS: '1.2.3.4', consts.ADDRESS: '1.2.3.4',
@ -445,6 +448,78 @@ class TestInterface(base.TestCase):
mock.call(["post-up", "eth1"]) mock.call(["post-up", "eth1"])
]) ])
@mock.patch('pyroute2.IPRoute.rule')
@mock.patch('pyroute2.IPRoute.route')
@mock.patch('pyroute2.IPRoute.addr')
@mock.patch('pyroute2.IPRoute.link')
@mock.patch('pyroute2.IPRoute.get_links')
@mock.patch('pyroute2.IPRoute.link_lookup')
@mock.patch('pyroute2.IPRoute.get_rules')
@mock.patch('subprocess.check_output')
def test_up_backend(self, mock_check_output, mock_get_rules,
mock_link_lookup, mock_get_links, mock_link, mock_addr,
mock_route, mock_rule):
iface = interface_file.InterfaceFile(
name="eth1",
if_type="backend",
mtu=1450,
addresses=[{
consts.ADDRESS: '1.2.3.4',
consts.PREFIXLEN: 24
}],
routes=[],
rules=[],
scripts={
consts.IFACE_UP: [{
consts.COMMAND: "post-up eth1"
}],
consts.IFACE_DOWN: [{
consts.COMMAND: "post-down eth1"
}],
})
idx = mock.MagicMock()
mock_link_lookup.return_value = [idx]
mock_get_links.return_value = [{
consts.STATE: consts.IFACE_DOWN
}]
mock_get_rules.return_value = [{
'src_len': 32,
'attrs': {
'FRA_SRC': '1.1.1.1',
'FRA_TABLE': 20,
'FRA_PROTOCOL': 0
}
}]
controller = interface.InterfaceController()
controller.up(iface)
mock_link.assert_called_once_with(
controller.SET,
index=idx,
state=consts.IFACE_UP,
mtu=1450)
mock_addr.assert_has_calls([
mock.call(controller.ADD,
index=idx,
address='1.2.3.4',
prefixlen=24,
family=socket.AF_INET),
])
mock_route.assert_called_once_with(
'dump', family=mock.ANY, match=mock.ANY)
# for 'backend' iface, we don't update the rules
mock_rule.assert_not_called()
mock_check_output.assert_has_calls([
mock.call(["post-up", "eth1"])
])
@mock.patch('pyroute2.IPRoute.rule') @mock.patch('pyroute2.IPRoute.rule')
@mock.patch('pyroute2.IPRoute.route') @mock.patch('pyroute2.IPRoute.route')
@mock.patch('pyroute2.IPRoute.addr') @mock.patch('pyroute2.IPRoute.addr')
@ -463,6 +538,7 @@ class TestInterface(base.TestCase):
mock_route, mock_rule): mock_route, mock_rule):
iface = interface_file.InterfaceFile( iface = interface_file.InterfaceFile(
name="eth1", name="eth1",
if_type="vip",
mtu=1450, mtu=1450,
addresses=[{ addresses=[{
consts.ADDRESS: '1.2.3.4', consts.ADDRESS: '1.2.3.4',
@ -658,6 +734,7 @@ class TestInterface(base.TestCase):
mock_route, mock_rule): mock_route, mock_rule):
iface = interface_file.InterfaceFile( iface = interface_file.InterfaceFile(
name="eth1", name="eth1",
if_type="vip",
mtu=1450, mtu=1450,
addresses=[{ addresses=[{
consts.DHCP: True, consts.DHCP: True,
@ -722,6 +799,7 @@ class TestInterface(base.TestCase):
mock_link, mock_addr, mock_route, mock_rule): mock_link, mock_addr, mock_route, mock_rule):
iface = interface_file.InterfaceFile( iface = interface_file.InterfaceFile(
name="eth1", name="eth1",
if_type="vip",
mtu=1450, mtu=1450,
addresses=[{ addresses=[{
consts.ADDRESS: '1.2.3.4', consts.ADDRESS: '1.2.3.4',
@ -848,6 +926,7 @@ class TestInterface(base.TestCase):
mock_addr, mock_route, mock_rule): mock_addr, mock_route, mock_rule):
iface = interface_file.InterfaceFile( iface = interface_file.InterfaceFile(
name="eth1", name="eth1",
if_type="vip",
mtu=1450, mtu=1450,
addresses=[{ addresses=[{
consts.ADDRESS: '1.2.3.4', consts.ADDRESS: '1.2.3.4',
@ -997,6 +1076,7 @@ class TestInterface(base.TestCase):
mock_route, mock_rule): mock_route, mock_rule):
iface = interface_file.InterfaceFile( iface = interface_file.InterfaceFile(
name="eth1", name="eth1",
if_type="vip",
mtu=1450, mtu=1450,
addresses=[{ addresses=[{
consts.ADDRESS: '1.2.3.4', consts.ADDRESS: '1.2.3.4',
@ -1069,6 +1149,7 @@ class TestInterface(base.TestCase):
mock_addr, mock_route, mock_rule): mock_addr, mock_route, mock_rule):
iface = interface_file.InterfaceFile( iface = interface_file.InterfaceFile(
name="eth1", name="eth1",
if_type="vip",
mtu=1450, mtu=1450,
addresses=[{ addresses=[{
consts.DHCP: True, consts.DHCP: True,

View File

@ -691,6 +691,7 @@ class TestInterfaceFile(base.TestCase):
'"prefixlen": 26}\n' '"prefixlen": 26}\n'
'],\n' '],\n'
'"mtu": 1450,\n' '"mtu": 1450,\n'
'"if_type": "mytype",\n'
'"name": "eth1",\n' '"name": "eth1",\n'
'"routes": [\n' '"routes": [\n'
'{"dst": "0.0.0.0/0",\n' '{"dst": "0.0.0.0/0",\n'
@ -717,6 +718,7 @@ class TestInterfaceFile(base.TestCase):
expected_dict = { expected_dict = {
consts.NAME: "eth1", consts.NAME: "eth1",
consts.IF_TYPE: "mytype",
consts.MTU: 1450, consts.MTU: 1450,
consts.ADDRESSES: [{ consts.ADDRESSES: [{
consts.ADDRESS: "10.0.0.181", consts.ADDRESS: "10.0.0.181",

View File

@ -23,8 +23,8 @@ class TestInterfaceCMD(base.TestCase):
def setUp(self): def setUp(self):
super().setUp() super().setUp()
self.interface1 = interface_file.InterfaceFile("eth1") self.interface1 = interface_file.InterfaceFile("eth1", if_type="type1")
self.interface2 = interface_file.InterfaceFile("eth2") self.interface2 = interface_file.InterfaceFile("eth2", if_type="type2")
def test_interfaces_find(self): def test_interfaces_find(self):
controller = mock.Mock() controller = mock.Mock()

View File

@ -0,0 +1,11 @@
---
fixes:
- |
Fixed a bug that could have made the VIP port unreachable because of the
removal of some IP rules in the Amphora. It could have been triggered only
when sending a request from a subnet that is not the VIP subnet but that is
plugged as a member subnet.
upgrade:
- |
A patch that fixes an issue making the VIP port unreachable because of
missing IP rules requires an update of the Amphora image.