Add nftables support for SR-IOV VIPs
This patch adds the initial nftables support in the amphora for SR-IOV VIPs. Followup patches will add rules to the nftables chain. As this point in the patch chain, SR-IOV VIPs will not pass any traffic. Change-Id: Ib2a1c3f49a26690d2e0e9c7330e047748c0b5105
This commit is contained in:
parent
75c1bdd104
commit
d83999f4ed
@ -274,6 +274,11 @@ OCTAVIA_REPO_PATH
|
|||||||
- Default: <directory above the script location>
|
- Default: <directory above the script location>
|
||||||
- Reference: https://github.com/openstack/octavia
|
- Reference: https://github.com/openstack/octavia
|
||||||
|
|
||||||
|
DIB_OCTAVIA_AMP_USE_NFTABLES
|
||||||
|
- Boolean that configures nftables inside the amphora image
|
||||||
|
- Required for SR-IOV enabled amphora
|
||||||
|
- Default: True
|
||||||
|
|
||||||
Using distribution packages for amphora agent
|
Using distribution packages for amphora agent
|
||||||
---------------------------------------------
|
---------------------------------------------
|
||||||
By default, amphora agent is installed from Octavia Git repository.
|
By default, amphora agent is installed from Octavia Git repository.
|
||||||
|
@ -310,7 +310,7 @@ else
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# Make sure we have a value set for DIB_OCTAVIA_AMP_USE_NFTABLES
|
# Make sure we have a value set for DIB_OCTAVIA_AMP_USE_NFTABLES
|
||||||
export DIB_OCTAVIA_AMP_USE_NFTABLES=${DIB_OCTAVIA_AMP_USE_NFTABLES:-False}
|
export DIB_OCTAVIA_AMP_USE_NFTABLES=${DIB_OCTAVIA_AMP_USE_NFTABLES:-True}
|
||||||
|
|
||||||
export CLOUD_INIT_DATASOURCES=${CLOUD_INIT_DATASOURCES:-"ConfigDrive"}
|
export CLOUD_INIT_DATASOURCES=${CLOUD_INIT_DATASOURCES:-"ConfigDrive"}
|
||||||
|
|
||||||
|
@ -86,4 +86,19 @@ Octavia flavor that will use the compute flavor.
|
|||||||
$ openstack loadbalancer flavorprofile create --name amphora-sriov-profile --provider amphora --flavor-data '{"compute_flavor": "amphora-sriov-flavor", "sriov_vip": true}'
|
$ openstack loadbalancer flavorprofile create --name amphora-sriov-profile --provider amphora --flavor-data '{"compute_flavor": "amphora-sriov-flavor", "sriov_vip": true}'
|
||||||
$ openstack loadbalancer flavor create --name SRIOV-public-members --flavorprofile amphora-sriov-profile --description "A load balancer that uses SR-IOV for the 'public' network and 'members' network." --enable
|
$ openstack loadbalancer flavor create --name SRIOV-public-members --flavorprofile amphora-sriov-profile --description "A load balancer that uses SR-IOV for the 'public' network and 'members' network." --enable
|
||||||
|
|
||||||
|
Building the Amphora Image
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Neutron does not support security groups on SR-IOV ports, so Octavia will use
|
||||||
|
nftables inside the Amphroa to provide network security. The amphora image
|
||||||
|
must be built with nftables enabled for SR-IOV enabled load balancers. Images
|
||||||
|
with nftables enabled can be used for both SR-IOV enabled load balancers as
|
||||||
|
well as load balancers that are not using SR-IOV ports. When the SR-IOV for
|
||||||
|
load balancer VIP ports feature was added to Octavia, the default setting for
|
||||||
|
using nftables has been changed to `True`. Prior to this it needed to be
|
||||||
|
enabled by setting an environment variable before building the Amphora image:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
$ export DIB_OCTAVIA_AMP_USE_NFTABLES=True
|
||||||
|
$ ./diskimage-create.sh
|
||||||
|
@ -64,14 +64,15 @@ class BaseOS(object):
|
|||||||
interface.write()
|
interface.write()
|
||||||
|
|
||||||
def write_vip_interface_file(self, interface, vips, mtu, vrrp_info,
|
def write_vip_interface_file(self, interface, vips, mtu, vrrp_info,
|
||||||
fixed_ips=None):
|
fixed_ips=None, is_sriov=False):
|
||||||
vip_interface = interface_file.VIPInterfaceFile(
|
vip_interface = interface_file.VIPInterfaceFile(
|
||||||
name=interface,
|
name=interface,
|
||||||
mtu=mtu,
|
mtu=mtu,
|
||||||
vips=vips,
|
vips=vips,
|
||||||
vrrp_info=vrrp_info,
|
vrrp_info=vrrp_info,
|
||||||
fixed_ips=fixed_ips,
|
fixed_ips=fixed_ips,
|
||||||
topology=CONF.controller_worker.loadbalancer_topology)
|
topology=CONF.controller_worker.loadbalancer_topology,
|
||||||
|
is_sriov=is_sriov)
|
||||||
vip_interface.write()
|
vip_interface.write()
|
||||||
|
|
||||||
def write_port_interface_file(self, interface, fixed_ips, mtu):
|
def write_port_interface_file(self, interface, fixed_ips, mtu):
|
||||||
|
@ -78,7 +78,7 @@ class Plug(object):
|
|||||||
|
|
||||||
def plug_vip(self, vip, subnet_cidr, gateway,
|
def plug_vip(self, vip, subnet_cidr, gateway,
|
||||||
mac_address, mtu=None, vrrp_ip=None, host_routes=(),
|
mac_address, mtu=None, vrrp_ip=None, host_routes=(),
|
||||||
additional_vips=()):
|
additional_vips=(), is_sriov=False):
|
||||||
vips = [{
|
vips = [{
|
||||||
'ip_address': vip,
|
'ip_address': vip,
|
||||||
'subnet_cidr': subnet_cidr,
|
'subnet_cidr': subnet_cidr,
|
||||||
@ -118,7 +118,8 @@ class Plug(object):
|
|||||||
interface=primary_interface,
|
interface=primary_interface,
|
||||||
vips=rendered_vips,
|
vips=rendered_vips,
|
||||||
mtu=mtu,
|
mtu=mtu,
|
||||||
vrrp_info=vrrp_info)
|
vrrp_info=vrrp_info,
|
||||||
|
is_sriov=is_sriov)
|
||||||
|
|
||||||
# Update the list of interfaces to add to the namespace
|
# Update the list of interfaces to add to the namespace
|
||||||
# This is used in the amphora reboot case to re-establish the namespace
|
# This is used in the amphora reboot case to re-establish the namespace
|
||||||
|
@ -203,7 +203,8 @@ class Server(object):
|
|||||||
net_info.get('mtu'),
|
net_info.get('mtu'),
|
||||||
net_info.get('vrrp_ip'),
|
net_info.get('vrrp_ip'),
|
||||||
net_info.get('host_routes', ()),
|
net_info.get('host_routes', ()),
|
||||||
net_info.get('additional_vips', ()))
|
net_info.get('additional_vips', ()),
|
||||||
|
net_info.get('is_sriov', False))
|
||||||
|
|
||||||
def plug_network(self):
|
def plug_network(self):
|
||||||
try:
|
try:
|
||||||
|
@ -28,6 +28,7 @@ from pyroute2.netlink.rtnl import ifaddrmsg
|
|||||||
from pyroute2.netlink.rtnl import rt_proto
|
from pyroute2.netlink.rtnl import rt_proto
|
||||||
|
|
||||||
from octavia.amphorae.backends.utils import interface_file
|
from octavia.amphorae.backends.utils import interface_file
|
||||||
|
from octavia.amphorae.backends.utils import nftable_utils
|
||||||
from octavia.common import constants as consts
|
from octavia.common import constants as consts
|
||||||
from octavia.common import exceptions
|
from octavia.common import exceptions
|
||||||
|
|
||||||
@ -175,9 +176,56 @@ class InterfaceController(object):
|
|||||||
ip_network = ipaddress.ip_network(address, strict=False)
|
ip_network = ipaddress.ip_network(address, strict=False)
|
||||||
return ip_network.compressed
|
return ip_network.compressed
|
||||||
|
|
||||||
|
def _setup_nftables_chain(self, interface):
|
||||||
|
# TODO(johnsom) Move this to pyroute2 when the nftables library
|
||||||
|
# improves.
|
||||||
|
|
||||||
|
# Create the nftable
|
||||||
|
cmd = [consts.NFT_CMD, consts.NFT_ADD, 'table', consts.NFT_FAMILY,
|
||||||
|
consts.NFT_VIP_TABLE]
|
||||||
|
try:
|
||||||
|
subprocess.check_output(cmd, stderr=subprocess.STDOUT)
|
||||||
|
except Exception as e:
|
||||||
|
if hasattr(e, 'output'):
|
||||||
|
LOG.error(e.output)
|
||||||
|
else:
|
||||||
|
LOG.error(e)
|
||||||
|
raise
|
||||||
|
|
||||||
|
# Create the chain with -310 priority to put it in front of the
|
||||||
|
# lvs-masquerade configured chain
|
||||||
|
cmd = [consts.NFT_CMD, consts.NFT_ADD, 'chain', consts.NFT_FAMILY,
|
||||||
|
consts.NFT_VIP_TABLE, consts.NFT_VIP_CHAIN,
|
||||||
|
'{', 'type', 'filter', 'hook', 'ingress', 'device',
|
||||||
|
interface.name, 'priority', consts.NFT_SRIOV_PRIORITY, ';',
|
||||||
|
'policy', 'drop', ';', '}']
|
||||||
|
try:
|
||||||
|
subprocess.check_output(cmd, stderr=subprocess.STDOUT)
|
||||||
|
except Exception as e:
|
||||||
|
if hasattr(e, 'output'):
|
||||||
|
LOG.error(e.output)
|
||||||
|
else:
|
||||||
|
LOG.error(e)
|
||||||
|
raise
|
||||||
|
|
||||||
|
nftable_utils.write_nftable_vip_rules_file(interface.name, [])
|
||||||
|
|
||||||
|
cmd = [consts.NFT_CMD, '-o', '-f', consts.NFT_VIP_RULES_FILE]
|
||||||
|
try:
|
||||||
|
subprocess.check_output(cmd, stderr=subprocess.STDOUT)
|
||||||
|
except Exception as e:
|
||||||
|
if hasattr(e, 'output'):
|
||||||
|
LOG.error(e.output)
|
||||||
|
else:
|
||||||
|
LOG.error(e)
|
||||||
|
raise
|
||||||
|
|
||||||
def up(self, interface):
|
def up(self, interface):
|
||||||
LOG.info("Setting interface %s up", interface.name)
|
LOG.info("Setting interface %s up", interface.name)
|
||||||
|
|
||||||
|
if interface.is_sriov:
|
||||||
|
self._setup_nftables_chain(interface)
|
||||||
|
|
||||||
with pyroute2.IPRoute() as ipr:
|
with pyroute2.IPRoute() as ipr:
|
||||||
idx = ipr.link_lookup(ifname=interface.name)[0]
|
idx = ipr.link_lookup(ifname=interface.name)[0]
|
||||||
|
|
||||||
|
@ -25,9 +25,8 @@ CONF = cfg.CONF
|
|||||||
|
|
||||||
|
|
||||||
class InterfaceFile(object):
|
class InterfaceFile(object):
|
||||||
def __init__(self, name, if_type,
|
def __init__(self, name, if_type, mtu=None, addresses=None,
|
||||||
mtu=None, addresses=None,
|
routes=None, rules=None, scripts=None, is_sriov=False):
|
||||||
routes=None, rules=None, scripts=None):
|
|
||||||
self.name = name
|
self.name = name
|
||||||
self.if_type = if_type
|
self.if_type = if_type
|
||||||
self.mtu = mtu
|
self.mtu = mtu
|
||||||
@ -38,6 +37,7 @@ class InterfaceFile(object):
|
|||||||
consts.IFACE_UP: [],
|
consts.IFACE_UP: [],
|
||||||
consts.IFACE_DOWN: []
|
consts.IFACE_DOWN: []
|
||||||
}
|
}
|
||||||
|
self.is_sriov = is_sriov
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_extensions(cls):
|
def get_extensions(cls):
|
||||||
@ -98,7 +98,8 @@ class InterfaceFile(object):
|
|||||||
consts.ADDRESSES: self.addresses,
|
consts.ADDRESSES: self.addresses,
|
||||||
consts.ROUTES: self.routes,
|
consts.ROUTES: self.routes,
|
||||||
consts.RULES: self.rules,
|
consts.RULES: self.rules,
|
||||||
consts.SCRIPTS: self.scripts
|
consts.SCRIPTS: self.scripts,
|
||||||
|
consts.IS_SRIOV: self.is_sriov
|
||||||
}
|
}
|
||||||
if self.mtu:
|
if self.mtu:
|
||||||
interface[consts.MTU] = self.mtu
|
interface[consts.MTU] = self.mtu
|
||||||
@ -106,12 +107,14 @@ 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,
|
||||||
|
is_sriov=False):
|
||||||
|
|
||||||
super().__init__(name, if_type=consts.VIP, mtu=mtu)
|
super().__init__(name, if_type=consts.VIP, mtu=mtu, is_sriov=is_sriov)
|
||||||
|
|
||||||
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)
|
||||||
|
|
||||||
if vrrp_info:
|
if vrrp_info:
|
||||||
self.addresses.append({
|
self.addresses.append({
|
||||||
consts.ADDRESS: vrrp_info['ip'],
|
consts.ADDRESS: vrrp_info['ip'],
|
||||||
|
63
octavia/amphorae/backends/utils/nftable_utils.py
Normal file
63
octavia/amphorae/backends/utils/nftable_utils.py
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
# Copyright 2024 Red Hat, 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 os
|
||||||
|
import stat
|
||||||
|
|
||||||
|
from octavia.common import constants as consts
|
||||||
|
|
||||||
|
|
||||||
|
def write_nftable_vip_rules_file(interface_name, rules):
|
||||||
|
flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC
|
||||||
|
# mode 00600
|
||||||
|
mode = stat.S_IRUSR | stat.S_IWUSR
|
||||||
|
|
||||||
|
# Create some strings shared on both code paths
|
||||||
|
table_string = f'table {consts.NFT_FAMILY} {consts.NFT_VIP_TABLE} {{\n'
|
||||||
|
chain_string = f' chain {consts.NFT_VIP_CHAIN} {{\n'
|
||||||
|
hook_string = (f' type filter hook ingress device {interface_name} '
|
||||||
|
f'priority {consts.NFT_SRIOV_PRIORITY}; policy drop;\n')
|
||||||
|
|
||||||
|
# Check if an existing rules file exists or we if need to create an
|
||||||
|
# "drop all" file with no rules except for VRRP. If it exists, we should
|
||||||
|
# not overwrite it here as it could be a reboot unless we were passed new
|
||||||
|
# rules.
|
||||||
|
if os.path.isfile(consts.NFT_VIP_RULES_FILE):
|
||||||
|
if not rules:
|
||||||
|
return
|
||||||
|
with os.fdopen(
|
||||||
|
os.open(consts.NFT_VIP_RULES_FILE, flags, mode), 'w') as file:
|
||||||
|
# Clear the existing rules in the kernel
|
||||||
|
# Note: The "nft -f" method is atomic, so clearing the rules will
|
||||||
|
# not leave the amphora exposed.
|
||||||
|
file.write(f'flush chain {consts.NFT_FAMILY} '
|
||||||
|
f'{consts.NFT_VIP_TABLE} {consts.NFT_VIP_CHAIN}\n')
|
||||||
|
file.write(table_string)
|
||||||
|
file.write(chain_string)
|
||||||
|
file.write(hook_string)
|
||||||
|
# TODO(johnsom) Add peer ports here consts.HAPROXY_BASE_PEER_PORT
|
||||||
|
# and ip protocol 112 for VRRP. Need the peer address
|
||||||
|
for rule in rules:
|
||||||
|
file.write(f' {rule}\n')
|
||||||
|
file.write(' }\n') # close the chain
|
||||||
|
file.write('}\n') # close the table
|
||||||
|
else: # No existing rules, create the "drop all" base rules
|
||||||
|
with os.fdopen(
|
||||||
|
os.open(consts.NFT_VIP_RULES_FILE, flags, mode), 'w') as file:
|
||||||
|
file.write(table_string)
|
||||||
|
file.write(chain_string)
|
||||||
|
file.write(hook_string)
|
||||||
|
# TODO(johnsom) Add peer ports here consts.HAPROXY_BASE_PEER_PORT
|
||||||
|
# and ip protocol 112 for VRRP. Need the peer address
|
||||||
|
file.write(' }\n') # close the chain
|
||||||
|
file.write('}\n') # close the table
|
@ -340,7 +340,7 @@ class HaproxyAmphoraLoadBalancerDriver(
|
|||||||
def finalize_amphora(self, amphora):
|
def finalize_amphora(self, amphora):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def _build_net_info(self, port, amphora, subnet, mtu=None):
|
def _build_net_info(self, port, amphora, subnet, mtu=None, sriov=False):
|
||||||
# NOTE(blogan): using the vrrp port here because that
|
# NOTE(blogan): using the vrrp port here because that
|
||||||
# is what the allowed address pairs network driver sets
|
# is what the allowed address pairs network driver sets
|
||||||
# this particular port to. This does expose a bit of
|
# this particular port to. This does expose a bit of
|
||||||
@ -359,7 +359,8 @@ class HaproxyAmphoraLoadBalancerDriver(
|
|||||||
'vrrp_ip': amphora[consts.VRRP_IP],
|
'vrrp_ip': amphora[consts.VRRP_IP],
|
||||||
'mtu': mtu or port[consts.NETWORK][consts.MTU],
|
'mtu': mtu or port[consts.NETWORK][consts.MTU],
|
||||||
'host_routes': host_routes,
|
'host_routes': host_routes,
|
||||||
'additional_vips': []}
|
'additional_vips': [],
|
||||||
|
'is_sriov': sriov}
|
||||||
return net_info
|
return net_info
|
||||||
|
|
||||||
def post_vip_plug(self, amphora, load_balancer, amphorae_network_config,
|
def post_vip_plug(self, amphora, load_balancer, amphorae_network_config,
|
||||||
@ -370,9 +371,12 @@ class HaproxyAmphoraLoadBalancerDriver(
|
|||||||
mtu = port[consts.NETWORK][consts.MTU]
|
mtu = port[consts.NETWORK][consts.MTU]
|
||||||
LOG.debug("Post-VIP-Plugging with vrrp_ip %s vrrp_port %s",
|
LOG.debug("Post-VIP-Plugging with vrrp_ip %s vrrp_port %s",
|
||||||
amphora.vrrp_ip, port[consts.ID])
|
amphora.vrrp_ip, port[consts.ID])
|
||||||
|
sriov = False
|
||||||
|
if load_balancer.vip.vnic_type == consts.VNIC_TYPE_DIRECT:
|
||||||
|
sriov = True
|
||||||
net_info = self._build_net_info(
|
net_info = self._build_net_info(
|
||||||
port, amphora.to_dict(),
|
port, amphora.to_dict(),
|
||||||
vip_subnet.to_dict(recurse=True), mtu)
|
vip_subnet.to_dict(recurse=True), mtu, sriov)
|
||||||
for add_vip in additional_vip_data:
|
for add_vip in additional_vip_data:
|
||||||
add_host_routes = [{'nexthop': hr.nexthop,
|
add_host_routes = [{'nexthop': hr.nexthop,
|
||||||
'destination': hr.destination}
|
'destination': hr.destination}
|
||||||
|
@ -366,6 +366,7 @@ ID = 'id'
|
|||||||
IMAGE_ID = 'image_id'
|
IMAGE_ID = 'image_id'
|
||||||
IP_ADDRESS = 'ip_address'
|
IP_ADDRESS = 'ip_address'
|
||||||
IPV6_ICMP = 'ipv6-icmp'
|
IPV6_ICMP = 'ipv6-icmp'
|
||||||
|
IS_SRIOV = 'is_sriov'
|
||||||
LB_NETWORK_IP = 'lb_network_ip'
|
LB_NETWORK_IP = 'lb_network_ip'
|
||||||
L7POLICY = 'l7policy'
|
L7POLICY = 'l7policy'
|
||||||
L7POLICY_ID = 'l7policy_id'
|
L7POLICY_ID = 'l7policy_id'
|
||||||
@ -967,3 +968,12 @@ IFLA_IFNAME = 'IFLA_IFNAME'
|
|||||||
|
|
||||||
# Amphora network directory
|
# Amphora network directory
|
||||||
AMP_NET_DIR_TEMPLATE = '/etc/octavia/interfaces/'
|
AMP_NET_DIR_TEMPLATE = '/etc/octavia/interfaces/'
|
||||||
|
|
||||||
|
# Amphora nftables constants
|
||||||
|
NFT_ADD = 'add'
|
||||||
|
NFT_CMD = '/usr/sbin/nft'
|
||||||
|
NFT_FAMILY = 'inet'
|
||||||
|
NFT_VIP_RULES_FILE = '/var/lib/octavia/nftables-vip.rules'
|
||||||
|
NFT_VIP_TABLE = 'amphora-vip'
|
||||||
|
NFT_VIP_CHAIN = 'amphora-vip-chain'
|
||||||
|
NFT_SRIOV_PRIORITY = '-310'
|
||||||
|
@ -163,7 +163,8 @@ class TestOSUtils(base.TestCase):
|
|||||||
mtu=MTU,
|
mtu=MTU,
|
||||||
vrrp_info=None,
|
vrrp_info=None,
|
||||||
fixed_ips=None,
|
fixed_ips=None,
|
||||||
topology="SINGLE")
|
topology="SINGLE",
|
||||||
|
is_sriov=False)
|
||||||
mock_vip_interface_file.return_value.write.assert_called_once()
|
mock_vip_interface_file.return_value.write.assert_called_once()
|
||||||
|
|
||||||
# Now test with an IPv6 VIP
|
# Now test with an IPv6 VIP
|
||||||
@ -193,7 +194,8 @@ class TestOSUtils(base.TestCase):
|
|||||||
mtu=MTU,
|
mtu=MTU,
|
||||||
vrrp_info=None,
|
vrrp_info=None,
|
||||||
fixed_ips=None,
|
fixed_ips=None,
|
||||||
topology="SINGLE")
|
topology="SINGLE",
|
||||||
|
is_sriov=False)
|
||||||
|
|
||||||
@mock.patch('octavia.amphorae.backends.utils.interface_file.'
|
@mock.patch('octavia.amphorae.backends.utils.interface_file.'
|
||||||
'PortInterfaceFile')
|
'PortInterfaceFile')
|
||||||
|
@ -448,6 +448,148 @@ class TestInterface(base.TestCase):
|
|||||||
mock.call(["post-up", "eth1"])
|
mock.call(["post-up", "eth1"])
|
||||||
])
|
])
|
||||||
|
|
||||||
|
@mock.patch('octavia.amphorae.backends.utils.nftable_utils.'
|
||||||
|
'write_nftable_vip_rules_file')
|
||||||
|
@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('subprocess.check_output')
|
||||||
|
def test_up_sriov(self, mock_check_output, mock_link_lookup,
|
||||||
|
mock_get_links, mock_link, mock_addr, mock_route,
|
||||||
|
mock_rule, mock_nftable):
|
||||||
|
iface = interface_file.InterfaceFile(
|
||||||
|
name="fake-eth1",
|
||||||
|
if_type="vip",
|
||||||
|
mtu=1450,
|
||||||
|
addresses=[{
|
||||||
|
consts.ADDRESS: '192.0.2.4',
|
||||||
|
consts.PREFIXLEN: 24
|
||||||
|
}, {
|
||||||
|
consts.ADDRESS: '198.51.100.4',
|
||||||
|
consts.PREFIXLEN: 16
|
||||||
|
}, {
|
||||||
|
consts.ADDRESS: '2001:db8::3',
|
||||||
|
consts.PREFIXLEN: 64
|
||||||
|
}],
|
||||||
|
routes=[{
|
||||||
|
consts.DST: '203.0.113.0/24',
|
||||||
|
consts.GATEWAY: '192.0.2.1',
|
||||||
|
consts.TABLE: 10,
|
||||||
|
consts.ONLINK: True
|
||||||
|
}, {
|
||||||
|
consts.DST: '198.51.100.0/24',
|
||||||
|
consts.GATEWAY: '192.0.2.2',
|
||||||
|
consts.PREFSRC: '192.0.2.4',
|
||||||
|
consts.SCOPE: 'link'
|
||||||
|
}, {
|
||||||
|
consts.DST: '2001:db8:2::1/128',
|
||||||
|
consts.GATEWAY: '2001:db8::1'
|
||||||
|
}],
|
||||||
|
rules=[{
|
||||||
|
consts.SRC: '203.0.113.1',
|
||||||
|
consts.SRC_LEN: 32,
|
||||||
|
consts.TABLE: 20,
|
||||||
|
}, {
|
||||||
|
consts.SRC: '2001:db8::1',
|
||||||
|
consts.SRC_LEN: 128,
|
||||||
|
consts.TABLE: 40,
|
||||||
|
}],
|
||||||
|
scripts={
|
||||||
|
consts.IFACE_UP: [{
|
||||||
|
consts.COMMAND: "post-up fake-eth1"
|
||||||
|
}],
|
||||||
|
consts.IFACE_DOWN: [{
|
||||||
|
consts.COMMAND: "post-down fake-eth1"
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
is_sriov=True)
|
||||||
|
|
||||||
|
idx = mock.MagicMock()
|
||||||
|
mock_link_lookup.return_value = [idx]
|
||||||
|
|
||||||
|
mock_get_links.return_value = [{
|
||||||
|
consts.STATE: consts.IFACE_DOWN
|
||||||
|
}]
|
||||||
|
|
||||||
|
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='192.0.2.4',
|
||||||
|
prefixlen=24,
|
||||||
|
family=socket.AF_INET),
|
||||||
|
mock.call(controller.ADD,
|
||||||
|
index=idx,
|
||||||
|
address='198.51.100.4',
|
||||||
|
prefixlen=16,
|
||||||
|
family=socket.AF_INET),
|
||||||
|
mock.call(controller.ADD,
|
||||||
|
index=idx,
|
||||||
|
address='2001:db8::3',
|
||||||
|
prefixlen=64,
|
||||||
|
family=socket.AF_INET6)
|
||||||
|
])
|
||||||
|
|
||||||
|
mock_route.assert_has_calls([
|
||||||
|
mock.call(controller.ADD,
|
||||||
|
oif=idx,
|
||||||
|
dst='203.0.113.0/24',
|
||||||
|
gateway='192.0.2.1',
|
||||||
|
table=10,
|
||||||
|
onlink=True,
|
||||||
|
family=socket.AF_INET),
|
||||||
|
mock.call(controller.ADD,
|
||||||
|
oif=idx,
|
||||||
|
dst='198.51.100.0/24',
|
||||||
|
gateway='192.0.2.2',
|
||||||
|
prefsrc='192.0.2.4',
|
||||||
|
scope='link',
|
||||||
|
family=socket.AF_INET),
|
||||||
|
mock.call(controller.ADD,
|
||||||
|
oif=idx,
|
||||||
|
dst='2001:db8:2::1/128',
|
||||||
|
gateway='2001:db8::1',
|
||||||
|
family=socket.AF_INET6)])
|
||||||
|
|
||||||
|
mock_rule.assert_has_calls([
|
||||||
|
mock.call(controller.ADD,
|
||||||
|
src="203.0.113.1",
|
||||||
|
src_len=32,
|
||||||
|
table=20,
|
||||||
|
family=socket.AF_INET),
|
||||||
|
mock.call(controller.ADD,
|
||||||
|
src="2001:db8::1",
|
||||||
|
src_len=128,
|
||||||
|
table=40,
|
||||||
|
family=socket.AF_INET6)])
|
||||||
|
|
||||||
|
mock_check_output.assert_has_calls([
|
||||||
|
mock.call([consts.NFT_CMD, consts.NFT_ADD, 'table',
|
||||||
|
consts.NFT_FAMILY, consts.NFT_VIP_TABLE], stderr=-2),
|
||||||
|
mock.call([consts.NFT_CMD, consts.NFT_ADD, 'chain',
|
||||||
|
consts.NFT_FAMILY, consts.NFT_VIP_TABLE,
|
||||||
|
consts.NFT_VIP_CHAIN, '{', 'type', 'filter', 'hook',
|
||||||
|
'ingress', 'device', 'fake-eth1', 'priority',
|
||||||
|
consts.NFT_SRIOV_PRIORITY, ';', 'policy', 'drop', ';',
|
||||||
|
'}'], stderr=-2),
|
||||||
|
mock.call([consts.NFT_CMD, '-o', '-f', consts.NFT_VIP_RULES_FILE],
|
||||||
|
stderr=-2),
|
||||||
|
mock.call(["post-up", "fake-eth1"])
|
||||||
|
])
|
||||||
|
|
||||||
|
mock_nftable.assert_called_once_with('fake-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')
|
||||||
|
106
octavia/tests/unit/amphorae/backends/utils/test_nftable_utils.py
Normal file
106
octavia/tests/unit/amphorae/backends/utils/test_nftable_utils.py
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
# Copyright 2024 Red Hat, 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 os
|
||||||
|
import stat
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
from octavia.amphorae.backends.utils import nftable_utils
|
||||||
|
from octavia.common import constants as consts
|
||||||
|
import octavia.tests.unit.base as base
|
||||||
|
|
||||||
|
|
||||||
|
class TestNFTableUtils(base.TestCase):
|
||||||
|
@mock.patch('os.open')
|
||||||
|
@mock.patch('os.path.isfile')
|
||||||
|
def test_write_nftable_vip_rules_file_exists(self, mock_isfile, mock_open):
|
||||||
|
"""Test when a rules file exists and no new rules
|
||||||
|
|
||||||
|
When an existing rules file is present and we call
|
||||||
|
write_nftable_vip_rules_file with no rules, the method should not
|
||||||
|
overwrite the existing rules.
|
||||||
|
"""
|
||||||
|
mock_isfile.return_value = True
|
||||||
|
|
||||||
|
nftable_utils.write_nftable_vip_rules_file('fake-eth2', [])
|
||||||
|
|
||||||
|
mock_open.assert_not_called()
|
||||||
|
|
||||||
|
@mock.patch('os.open')
|
||||||
|
@mock.patch('os.path.isfile')
|
||||||
|
def test_write_nftable_vip_rules_file_rules(self, mock_isfile,
|
||||||
|
mock_open):
|
||||||
|
"""Test when a rules file exists and rules are passed in
|
||||||
|
|
||||||
|
This should create a simple rules file with the base chain and rules.
|
||||||
|
"""
|
||||||
|
mock_isfile.return_value = True
|
||||||
|
mock_open.return_value = 'fake-fd'
|
||||||
|
|
||||||
|
mocked_open = mock.mock_open()
|
||||||
|
with mock.patch.object(os, 'fdopen', mocked_open):
|
||||||
|
nftable_utils.write_nftable_vip_rules_file(
|
||||||
|
'fake-eth2', ['test rule 1', 'test rule 2'])
|
||||||
|
|
||||||
|
mocked_open.assert_called_once_with('fake-fd', 'w')
|
||||||
|
mock_open.assert_called_once_with(
|
||||||
|
consts.NFT_VIP_RULES_FILE,
|
||||||
|
(os.O_WRONLY | os.O_CREAT | os.O_TRUNC),
|
||||||
|
(stat.S_IRUSR | stat.S_IWUSR))
|
||||||
|
|
||||||
|
handle = mocked_open()
|
||||||
|
handle.write.assert_has_calls([
|
||||||
|
mock.call(f'flush chain {consts.NFT_FAMILY} '
|
||||||
|
f'{consts.NFT_VIP_TABLE} {consts.NFT_VIP_CHAIN}\n'),
|
||||||
|
mock.call(f'table {consts.NFT_FAMILY} {consts.NFT_VIP_TABLE} '
|
||||||
|
'{\n'),
|
||||||
|
mock.call(f' chain {consts.NFT_VIP_CHAIN} {{\n'),
|
||||||
|
mock.call(' type filter hook ingress device fake-eth2 '
|
||||||
|
f'priority {consts.NFT_SRIOV_PRIORITY}; policy drop;\n'),
|
||||||
|
mock.call(' test rule 1\n'),
|
||||||
|
mock.call(' test rule 2\n'),
|
||||||
|
mock.call(' }\n'),
|
||||||
|
mock.call('}\n')
|
||||||
|
])
|
||||||
|
|
||||||
|
@mock.patch('os.open')
|
||||||
|
@mock.patch('os.path.isfile')
|
||||||
|
def test_write_nftable_vip_rules_file_missing(self, mock_isfile,
|
||||||
|
mock_open):
|
||||||
|
"""Test when a rules file does not exist and no new rules
|
||||||
|
|
||||||
|
This should create a simple rules file with the base chain.
|
||||||
|
"""
|
||||||
|
mock_isfile.return_value = False
|
||||||
|
mock_open.return_value = 'fake-fd'
|
||||||
|
|
||||||
|
mocked_open = mock.mock_open()
|
||||||
|
with mock.patch.object(os, 'fdopen', mocked_open):
|
||||||
|
nftable_utils.write_nftable_vip_rules_file('fake-eth2', [])
|
||||||
|
|
||||||
|
mocked_open.assert_called_once_with('fake-fd', 'w')
|
||||||
|
mock_open.assert_called_once_with(
|
||||||
|
consts.NFT_VIP_RULES_FILE,
|
||||||
|
(os.O_WRONLY | os.O_CREAT | os.O_TRUNC),
|
||||||
|
(stat.S_IRUSR | stat.S_IWUSR))
|
||||||
|
|
||||||
|
handle = mocked_open()
|
||||||
|
handle.write.assert_has_calls([
|
||||||
|
mock.call(f'table {consts.NFT_FAMILY} {consts.NFT_VIP_TABLE} '
|
||||||
|
'{\n'),
|
||||||
|
mock.call(f' chain {consts.NFT_VIP_CHAIN} {{\n'),
|
||||||
|
mock.call(' type filter hook ingress device fake-eth2 '
|
||||||
|
f'priority {consts.NFT_SRIOV_PRIORITY}; policy drop;\n'),
|
||||||
|
mock.call(' }\n'),
|
||||||
|
mock.call('}\n')
|
||||||
|
])
|
@ -113,7 +113,8 @@ class TestHaproxyAmphoraLoadBalancerDriverTest(base.TestCase):
|
|||||||
'vrrp_ip': self.amp.vrrp_ip,
|
'vrrp_ip': self.amp.vrrp_ip,
|
||||||
'mtu': FAKE_MTU,
|
'mtu': FAKE_MTU,
|
||||||
'host_routes': host_routes_data,
|
'host_routes': host_routes_data,
|
||||||
'additional_vips': []}
|
'additional_vips': [],
|
||||||
|
'is_sriov': False}
|
||||||
|
|
||||||
self.timeout_dict = {constants.REQ_CONN_TIMEOUT: 1,
|
self.timeout_dict = {constants.REQ_CONN_TIMEOUT: 1,
|
||||||
constants.REQ_READ_TIMEOUT: 2,
|
constants.REQ_READ_TIMEOUT: 2,
|
||||||
@ -766,6 +767,7 @@ class TestHaproxyAmphoraLoadBalancerDriverTest(base.TestCase):
|
|||||||
'host_routes': netinfo['host_routes']
|
'host_routes': netinfo['host_routes']
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
netinfo['is_sriov'] = False
|
||||||
self.driver.clients[API_VERSION].plug_vip.assert_called_once_with(
|
self.driver.clients[API_VERSION].plug_vip.assert_called_once_with(
|
||||||
self.amp, self.lb.vip.ip_address, netinfo)
|
self.amp, self.lb.vip.ip_address, netinfo)
|
||||||
|
|
||||||
@ -815,7 +817,8 @@ class TestHaproxyAmphoraLoadBalancerDriverTest(base.TestCase):
|
|||||||
vrrp_ip=self.amp.vrrp_ip,
|
vrrp_ip=self.amp.vrrp_ip,
|
||||||
host_routes=[],
|
host_routes=[],
|
||||||
additional_vips=[],
|
additional_vips=[],
|
||||||
mtu=FAKE_MTU
|
mtu=FAKE_MTU,
|
||||||
|
is_sriov=False
|
||||||
)))
|
)))
|
||||||
|
|
||||||
def test_post_network_plug_with_host_routes(self):
|
def test_post_network_plug_with_host_routes(self):
|
||||||
|
@ -670,9 +670,11 @@ def sample_vrrp_group_tuple():
|
|||||||
smtp_connect_timeout='')
|
smtp_connect_timeout='')
|
||||||
|
|
||||||
|
|
||||||
def sample_vip_tuple(ip_address='10.0.0.2', subnet_id='vip_subnet_uuid'):
|
def sample_vip_tuple(ip_address='10.0.0.2', subnet_id='vip_subnet_uuid',
|
||||||
vip = collections.namedtuple('vip', ('ip_address', 'subnet_id'))
|
vnic_type='normal'):
|
||||||
return vip(ip_address=ip_address, subnet_id=subnet_id)
|
vip = collections.namedtuple('vip', ('ip_address', 'subnet_id',
|
||||||
|
'vnic_type'))
|
||||||
|
return vip(ip_address=ip_address, subnet_id=subnet_id, vnic_type=vnic_type)
|
||||||
|
|
||||||
|
|
||||||
def sample_listener_tuple(proto=None, monitor=True, alloc_default_pool=True,
|
def sample_listener_tuple(proto=None, monitor=True, alloc_default_pool=True,
|
||||||
|
@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
other:
|
||||||
|
- |
|
||||||
|
Amphora images will now be built with nftables by default.
|
Loading…
Reference in New Issue
Block a user