Add generic network interface management in the amphora

Handle network configuration using Octavia tools.

amphora-interface configures network interfaces inside the amphora
using pyroute2 and a set of json files for persistent configuration in
the /etc/octavia/interfaces/ directory.

Story: 2005235
Task: 30019

Depends-On: https://review.opendev.org/806558

Change-Id: I5360c8246cd39f90eb7104a883f87c0042d146c4
This commit is contained in:
Gregory Thiemonge 2020-10-30 20:20:26 +01:00
parent 39735ebf10
commit 5dd7ad9ad8
38 changed files with 2858 additions and 1866 deletions

View File

@ -27,6 +27,9 @@ ln -s $AMP_VENV/bin/haproxy-vrrp-* /usr/local/bin/ || true
# Link heath checker script
ln -s $AMP_VENV/bin/amphora-health-checker /usr/local/bin/amphora-health-checker || true
# Link amphora interface script
ln -s $AMP_VENV/bin/amphora-interface /usr/local/bin/amphora-interface || true
mkdir /etc/octavia
# we assume certs, etc will come in through the config drive
mkdir /etc/octavia/certs

View File

@ -457,7 +457,6 @@
#
# agent_server_network_dir =
# agent_server_network_file =
# agent_request_read_timeout = 180
# Minimum TLS protocol, eg: TLS, TLSv1.1, TLSv1.2, TLSv1.3 (if available)

View File

@ -40,8 +40,6 @@ class AgentJinjaTemplater(object):
'agent_server_cert': CONF.amphora_agent.agent_server_cert,
'agent_server_network_dir':
CONF.amphora_agent.agent_server_network_dir,
'agent_server_network_file':
CONF.amphora_agent.agent_server_network_file,
'agent_request_read_timeout':
CONF.amphora_agent.agent_request_read_timeout,
'amphora_id': amphora_id,

View File

@ -28,7 +28,6 @@ import webob
from werkzeug import exceptions
from octavia.amphorae.backends.agent.api_server import haproxy_compatibility
from octavia.amphorae.backends.agent.api_server import osutils
from octavia.amphorae.backends.agent.api_server import util
from octavia.common import constants as consts
from octavia.common import utils as octavia_utils
@ -73,9 +72,6 @@ class Wrapped(object):
class Loadbalancer(object):
def __init__(self):
self._osutils = osutils.BaseOS.get_os_util()
def get_haproxy_config(self, lb_id):
"""Gets the haproxy config
@ -199,7 +195,6 @@ class Loadbalancer(object):
respawn_interval),
amphora_netns=consts.AMP_NETNS_SVC_PREFIX,
amphora_nsname=consts.AMPHORA_NAMESPACE,
HasIFUPAll=self._osutils.has_ifup_all(),
haproxy_major_version=hap_major,
haproxy_minor_version=hap_minor
)

View File

@ -12,31 +12,22 @@
# License for the specific language governing permissions and limitations
# under the License.
import errno
import ipaddress
import os
import shutil
import stat
import subprocess
import distro
import jinja2
from oslo_config import cfg
from oslo_log import log as logging
import webob
from werkzeug import exceptions
from octavia.amphorae.backends.utils import interface_file
from octavia.common import constants as consts
from octavia.common import exceptions as octavia_exceptions
from octavia.common import utils
CONF = cfg.CONF
LOG = logging.getLogger(__name__)
j2_env = jinja2.Environment(autoescape=True, loader=jinja2.FileSystemLoader(
os.path.dirname(os.path.realpath(__file__)) + consts.AGENT_API_TEMPLATES))
class BaseOS(object):
@ -62,160 +53,51 @@ class BaseOS(object):
def _map_package_name(self, package_name):
return self.package_name_map.get(package_name, package_name)
def get_network_interface_file(self, interface):
if CONF.amphora_agent.agent_server_network_file:
return CONF.amphora_agent.agent_server_network_file
if CONF.amphora_agent.agent_server_network_dir:
return os.path.join(CONF.amphora_agent.agent_server_network_dir,
interface)
network_dir = consts.UBUNTU_AMP_NET_DIR_TEMPLATE.format(
netns=consts.AMPHORA_NAMESPACE)
return os.path.join(network_dir, interface)
def create_netns_dir(self, network_dir, netns_network_dir, ignore=None):
# We need to setup the netns network directory so that the ifup
# commands used here and in the startup scripts "sees" the right
# interfaces and scripts.
try:
os.makedirs('/etc/netns/' + consts.AMPHORA_NAMESPACE)
shutil.copytree(
network_dir,
'/etc/netns/{netns}/{net_dir}'.format(
netns=consts.AMPHORA_NAMESPACE,
net_dir=netns_network_dir),
symlinks=True,
ignore=ignore)
except OSError as e:
# Raise the error if it's not "File exists" otherwise pass
if e.errno != errno.EEXIST:
raise
def write_vip_interface_file(self, interface_file_path,
primary_interface, vip, ip, broadcast,
netmask, gateway, mtu, vrrp_ip, vrrp_version,
render_host_routes, template_vip):
# write interface file
mode = stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH
# If we are using a consolidated interfaces file, just append
# otherwise clear the per interface file as we are rewriting it
# TODO(johnsom): We need a way to clean out old interfaces records
if CONF.amphora_agent.agent_server_network_file:
flags = os.O_WRONLY | os.O_CREAT | os.O_APPEND
else:
flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC
with os.fdopen(os.open(interface_file_path, flags, mode),
'w') as text_file:
text = template_vip.render(
consts=consts,
interface=primary_interface,
vip=vip,
vip_ipv6=ip.version == 6,
# For ipv6 the netmask is already the prefix
prefix=(netmask if ip.version == 6
else utils.netmask_to_prefix(netmask)),
broadcast=broadcast,
netmask=netmask,
gateway=gateway,
network=utils.ip_netmask_to_cidr(vip, netmask),
mtu=mtu,
vrrp_ip=vrrp_ip,
vrrp_ipv6=vrrp_version == 6,
host_routes=render_host_routes,
topology=CONF.controller_worker.loadbalancer_topology,
def write_interface_file(self, interface, ip_address, prefixlen):
interface = interface_file.InterfaceFile(
name=interface,
addresses=[{
"address": ip_address,
"prefixlen": prefixlen
}]
)
text_file.write(text)
interface.write()
def write_port_interface_file(self, netns_interface, fixed_ips, mtu,
interface_file_path, template_port):
# write interface file
# If we are using a consolidated interfaces file, just append
# otherwise clear the per interface file as we are rewriting it
# TODO(johnsom): We need a way to clean out old interfaces records
if CONF.amphora_agent.agent_server_network_file:
flags = os.O_WRONLY | os.O_CREAT | os.O_APPEND
else:
flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC
# mode 00644
mode = stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH
with os.fdopen(os.open(interface_file_path, flags, mode),
'w') as text_file:
text = self._generate_network_file_text(
netns_interface, fixed_ips, mtu, template_port)
text_file.write(text)
@classmethod
def _generate_network_file_text(cls, netns_interface, fixed_ips, mtu,
template_port):
text = ''
if fixed_ips is None:
text = template_port.render(interface=netns_interface)
else:
for index, fixed_ip in enumerate(fixed_ips, -1):
try:
ip_addr = fixed_ip['ip_address']
cidr = fixed_ip['subnet_cidr']
ip = ipaddress.ip_address(ip_addr)
network = ipaddress.ip_network(cidr)
broadcast = network.broadcast_address.exploded
netmask = (network.prefixlen if ip.version == 6
else network.netmask.exploded)
host_routes = cls.get_host_routes(fixed_ip)
except ValueError:
return webob.Response(
json=dict(message="Invalid network IP"), status=400)
new_text = template_port.render(interface=netns_interface,
ipv6=ip.version == 6,
ip_address=ip.exploded,
broadcast=broadcast,
netmask=netmask,
def write_vip_interface_file(self, interface, vip, ip_version,
prefixlen, gateway,
mtu, vrrp_ip,
host_routes):
vip_interface = interface_file.VIPInterfaceFile(
name=interface,
mtu=mtu,
host_routes=host_routes)
text = '\n'.join([text, new_text])
return text
vip=vip,
ip_version=ip_version,
prefixlen=prefixlen,
gateway=gateway,
vrrp_ip=vrrp_ip,
host_routes=host_routes,
topology=CONF.controller_worker.loadbalancer_topology)
vip_interface.write()
def write_port_interface_file(self, interface, fixed_ips, mtu):
port_interface = interface_file.PortInterfaceFile(
name=interface,
mtu=mtu,
fixed_ips=fixed_ips)
port_interface.write()
@classmethod
def get_host_routes(cls, fixed_ip):
host_routes = []
for hr in fixed_ip.get('host_routes', []):
network = ipaddress.ip_network(hr['destination'])
host_routes.append({'network': network, 'gw': hr['nexthop']})
return host_routes
@classmethod
def _bring_if_up(cls, interface, what, flush=True):
# Note, we are not using pyroute2 for this as it is not /etc/netns
# aware.
# Work around for bug:
# https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=845121
int_up = "ip netns exec {ns} ip link set {int} up".format(
ns=consts.AMPHORA_NAMESPACE, int=interface)
addr_flush = "ip netns exec {ns} ip addr flush {int}".format(
ns=consts.AMPHORA_NAMESPACE, int=interface)
cmd = ("ip netns exec {ns} ifup {params}".format(
def _bring_if_up(cls, interface, what):
cmd = ("ip netns exec {ns} amphora-interface up {params}".format(
ns=consts.AMPHORA_NAMESPACE, params=interface))
LOG.debug("Executing: %s", cmd)
try:
out = subprocess.check_output(int_up.split(),
stderr=subprocess.STDOUT)
LOG.debug(out)
if flush:
out = subprocess.check_output(addr_flush.split(),
stderr=subprocess.STDOUT)
LOG.debug(out)
out = subprocess.check_output(cmd.split(),
stderr=subprocess.STDOUT)
LOG.debug(out)
except subprocess.CalledProcessError as e:
LOG.error('Failed to ifup %s due to error: %s %s', interface, e,
e.output)
LOG.error('Failed to set up %s due to error: %s %s', interface,
e, e.output)
raise exceptions.HTTPException(
response=webob.Response(json=dict(
message='Error plugging {0}'.format(what),
@ -223,34 +105,23 @@ class BaseOS(object):
@classmethod
def _bring_if_down(cls, interface):
# Note, we are not using pyroute2 for this as it is not /etc/netns
# aware.
cmd = ("ip netns exec {ns} ifdown {params}".format(
cmd = ("ip netns exec {ns} amphora-interface down {params}".format(
ns=consts.AMPHORA_NAMESPACE, params=interface))
LOG.debug("Executing: %s", cmd)
try:
subprocess.check_output(cmd.split(), stderr=subprocess.STDOUT)
except subprocess.CalledProcessError as e:
LOG.info('Ignoring failure to ifdown %s due to error: %s %s',
LOG.info('Ignoring failure to set %s down due to error: %s %s',
interface, e, e.output)
@classmethod
def bring_interfaces_up(cls, ip, primary_interface, secondary_interface):
def bring_interfaces_up(cls, ip, primary_interface):
cls._bring_if_down(primary_interface)
if secondary_interface:
cls._bring_if_down(secondary_interface)
cls._bring_if_up(primary_interface, 'VIP')
if secondary_interface:
cls._bring_if_up(secondary_interface, 'VIP', flush=False)
def has_ifup_all(self):
return True
class Ubuntu(BaseOS):
ETH_X_PORT_CONF = 'plug_port_ethX.conf.j2'
ETH_X_VIP_CONF = 'plug_vip_ethX.conf.j2'
@classmethod
def is_os_name(cls, os_name):
return os_name in ['ubuntu']
@ -259,87 +130,9 @@ class Ubuntu(BaseOS):
name = self._map_package_name(package_name)
return "dpkg-query -W -f=${{Version}} {name}".format(name=name)
def get_network_interface_file(self, interface):
if CONF.amphora_agent.agent_server_network_file:
return CONF.amphora_agent.agent_server_network_file
if CONF.amphora_agent.agent_server_network_dir:
return os.path.join(CONF.amphora_agent.agent_server_network_dir,
interface + '.cfg')
network_dir = consts.UBUNTU_AMP_NET_DIR_TEMPLATE.format(
netns=consts.AMPHORA_NAMESPACE)
return os.path.join(network_dir, interface + '.cfg')
def get_network_path(self):
return '/etc/network'
def get_netns_network_dir(self):
network_dir = self.get_network_path()
return os.path.basename(network_dir)
def create_netns_dir(
self, network_dir=None, netns_network_dir=None, ignore=None):
if not netns_network_dir:
netns_network_dir = self.get_netns_network_dir()
if not network_dir:
network_dir = self.get_network_path()
if not ignore:
ignore = shutil.ignore_patterns('eth0*', 'openssh*')
super().create_netns_dir(
network_dir, netns_network_dir, ignore)
def write_interfaces_file(self):
name = '/etc/netns/{}/network/interfaces'.format(
consts.AMPHORA_NAMESPACE)
flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC
# mode 00644
mode = stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH
with os.fdopen(os.open(name, flags, mode), 'w') as int_file:
int_file.write('auto lo\n')
int_file.write('iface lo inet loopback\n')
if not CONF.amphora_agent.agent_server_network_file:
int_file.write('source /etc/netns/{}/network/'
'interfaces.d/*.cfg\n'.format(
consts.AMPHORA_NAMESPACE))
def write_vip_interface_file(self, interface_file_path,
primary_interface, vip, ip, broadcast,
netmask, gateway, mtu, vrrp_ip, vrrp_version,
render_host_routes, template_vip=None):
if not template_vip:
template_vip = j2_env.get_template(self.ETH_X_VIP_CONF)
super().write_vip_interface_file(
interface_file_path, primary_interface, vip, ip, broadcast,
netmask, gateway, mtu, vrrp_ip, vrrp_version, render_host_routes,
template_vip)
def write_port_interface_file(self, netns_interface, fixed_ips, mtu,
interface_file_path=None,
template_port=None):
if not interface_file_path:
interface_file_path = self.get_network_interface_file(
netns_interface)
if not template_port:
template_port = j2_env.get_template(self.ETH_X_PORT_CONF)
super().write_port_interface_file(
netns_interface, fixed_ips, mtu, interface_file_path,
template_port)
def has_ifup_all(self):
return True
class RH(BaseOS):
ETH_X_PORT_CONF = 'rh_plug_port_ethX.conf.j2'
ETH_X_VIP_CONF = 'rh_plug_vip_ethX.conf.j2'
ETH_X_ALIAS_VIP_CONF = 'rh_plug_vip_ethX_alias.conf.j2'
ROUTE_ETH_X_CONF = 'rh_route_ethX.conf.j2'
RULE_ETH_X_CONF = 'rh_rule_ethX.conf.j2'
# The reason of make them as jinja templates is the current scripts force
# to add the iptables, so leave it now for future extending if possible.
ETH_IFUP_LOCAL_SCRIPT = 'rh_plug_port_eth_ifup_local.conf.j2'
ETH_IFDOWN_LOCAL_SCRIPT = 'rh_plug_port_eth_ifdown_local.conf.j2'
@classmethod
def is_os_name(cls, os_name):
return os_name in ['fedora', 'rhel']
@ -348,227 +141,6 @@ class RH(BaseOS):
name = self._map_package_name(package_name)
return "rpm -q --queryformat %{{VERSION}} {name}".format(name=name)
@staticmethod
def _get_network_interface_file(prefix, interface):
if CONF.amphora_agent.agent_server_network_file:
return CONF.amphora_agent.agent_server_network_file
if CONF.amphora_agent.agent_server_network_dir:
network_dir = CONF.amphora_agent.agent_server_network_dir
else:
network_dir = consts.RH_AMP_NET_DIR_TEMPLATE.format(
netns=consts.AMPHORA_NAMESPACE)
return os.path.join(network_dir, prefix + interface)
def get_network_interface_file(self, interface):
return self._get_network_interface_file('ifcfg-', interface)
def get_alias_network_interface_file(self, interface):
return self.get_network_interface_file(interface + ':0')
def get_static_routes_interface_file(self, interface, version):
route = 'route6-' if version == 6 else 'route-'
return self._get_network_interface_file(route, interface)
def get_route_rules_interface_file(self, interface, version):
rule = 'rule6-' if version == 6 else 'rule-'
return self._get_network_interface_file(rule, interface)
def get_network_path(self):
return '/etc/sysconfig/network-scripts'
def get_netns_network_dir(self):
network_full_path = self.get_network_path()
network_basename = os.path.basename(network_full_path)
network_dirname = os.path.dirname(network_full_path)
network_prefixdir = os.path.basename(network_dirname)
return os.path.join(network_prefixdir, network_basename)
def create_netns_dir(
self, network_dir=None, netns_network_dir=None, ignore=None):
if not netns_network_dir:
netns_network_dir = self.get_netns_network_dir()
if not network_dir:
network_dir = self.get_network_path()
if not ignore:
ignore = shutil.ignore_patterns('ifcfg-eth0*')
super().create_netns_dir(
network_dir, netns_network_dir, ignore)
# Copy /etc/sysconfig/network file
src = '/etc/sysconfig/network'
dst = '/etc/netns/{netns}/sysconfig'.format(
netns=consts.AMPHORA_NAMESPACE)
shutil.copy2(src, dst)
def write_interfaces_file(self):
# No interfaces file in RH based flavors
return
def write_vip_interface_file(self, interface_file_path,
primary_interface, vip, ip, broadcast,
netmask, gateway, mtu, vrrp_ip, vrrp_version,
render_host_routes, template_vip=None):
if not template_vip:
template_vip = j2_env.get_template(self.ETH_X_VIP_CONF)
super().write_vip_interface_file(
interface_file_path, primary_interface, vip, ip, broadcast,
netmask, gateway, mtu, vrrp_ip, vrrp_version, render_host_routes,
template_vip)
# keepalived will handle the VIP if we are on active/standby
if (ip.version == 4 and
CONF.controller_worker.loadbalancer_topology ==
consts.TOPOLOGY_SINGLE):
# Create an IPv4 alias interface, needed in RH based flavors
alias_interface_file_path = self.get_alias_network_interface_file(
primary_interface)
template_vip_alias = j2_env.get_template(self.ETH_X_ALIAS_VIP_CONF)
super().write_vip_interface_file(
alias_interface_file_path, primary_interface, vip, ip,
broadcast, netmask, gateway, mtu, vrrp_ip, vrrp_version,
render_host_routes, template_vip_alias)
routes_interface_file_path = (
self.get_static_routes_interface_file(primary_interface,
ip.version))
template_routes = j2_env.get_template(self.ROUTE_ETH_X_CONF)
self.write_static_routes_interface_file(
routes_interface_file_path, primary_interface,
render_host_routes, template_routes, gateway, vip, netmask)
# keepalived will handle the rule(s) if we are on actvie/standby
if (CONF.controller_worker.loadbalancer_topology ==
consts.TOPOLOGY_SINGLE):
route_rules_interface_file_path = (
self.get_route_rules_interface_file(primary_interface,
ip.version))
template_rules = j2_env.get_template(self.RULE_ETH_X_CONF)
self.write_static_routes_interface_file(
route_rules_interface_file_path, primary_interface,
render_host_routes, template_rules, gateway, vip, netmask)
self._write_ifup_ifdown_local_scripts_if_possible()
def write_static_routes_interface_file(self, interface_file_path,
interface, host_routes,
template_routes, gateway,
vip, netmask):
# write static routes interface file
mode = stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH
# TODO(johnsom): We need a way to clean out old interfaces records
if CONF.amphora_agent.agent_server_network_file:
flags = os.O_WRONLY | os.O_CREAT | os.O_APPEND
else:
flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC
with os.fdopen(os.open(interface_file_path, flags, mode),
'w') as text_file:
text = template_routes.render(
consts=consts,
interface=interface,
host_routes=host_routes,
gateway=gateway,
network=utils.ip_netmask_to_cidr(vip, netmask),
vip=vip,
topology=CONF.controller_worker.loadbalancer_topology,
)
text_file.write(text)
def write_port_interface_file(self, netns_interface, fixed_ips, mtu,
interface_file_path=None,
template_port=None):
if not interface_file_path:
interface_file_path = self.get_network_interface_file(
netns_interface)
if not template_port:
template_port = j2_env.get_template(self.ETH_X_PORT_CONF)
super().write_port_interface_file(
netns_interface, fixed_ips, mtu, interface_file_path,
template_port)
if fixed_ips:
host_routes = []
host_routes_ipv6 = []
for fixed_ip in fixed_ips:
ip_addr = fixed_ip['ip_address']
ip = ipaddress.ip_address(ip_addr)
if ip.version == 6:
host_routes_ipv6.extend(self.get_host_routes(fixed_ip))
else:
host_routes.extend(self.get_host_routes(fixed_ip))
routes_interface_file_path = (
self.get_static_routes_interface_file(netns_interface, 4))
template_routes = j2_env.get_template(self.ROUTE_ETH_X_CONF)
self.write_static_routes_interface_file(
routes_interface_file_path, netns_interface,
host_routes, template_routes, None, None, None)
routes_interface_file_path_ipv6 = (
self.get_static_routes_interface_file(netns_interface, 6))
template_routes = j2_env.get_template(self.ROUTE_ETH_X_CONF)
self.write_static_routes_interface_file(
routes_interface_file_path_ipv6, netns_interface,
host_routes_ipv6, template_routes, None, None, None)
self._write_ifup_ifdown_local_scripts_if_possible()
@classmethod
def bring_interfaces_up(cls, ip, primary_interface, secondary_interface):
if ip.version == 4:
super(RH, cls).bring_interfaces_up(
ip, primary_interface, secondary_interface)
else:
# Secondary interface is not present in IPv6 configuration
cls._bring_if_down(primary_interface)
cls._bring_if_up(primary_interface, 'VIP')
def has_ifup_all(self):
return False
def _write_ifup_ifdown_local_scripts_if_possible(self):
if self._check_ifup_ifdown_local_scripts_exists():
template_ifup_local = j2_env.get_template(
self.ETH_IFUP_LOCAL_SCRIPT)
self.write_port_interface_if_local_scripts(template_ifup_local)
template_ifdown_local = j2_env.get_template(
self.ETH_IFDOWN_LOCAL_SCRIPT)
self.write_port_interface_if_local_scripts(template_ifdown_local,
ifup=False)
def _check_ifup_ifdown_local_scripts_exists(self):
file_names = ['ifup-local', 'ifdown-local']
target_dir = '/sbin/'
res = []
for file_name in file_names:
if os.path.exists(os.path.join(target_dir, file_name)):
res.append(True)
else:
res.append(False)
# This means we only add the scripts when both of them are non-exists
return not any(res)
def write_port_interface_if_local_scripts(
self, template_script, ifup=True):
file_name = 'ifup' + '-local'
mode = stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH
flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC
if not ifup:
file_name = 'ifdown' + '-local'
with os.fdopen(
os.open(os.path.join(
'/sbin/', file_name), flags, mode), 'w') as text_file:
text = template_script.render()
text_file.write(text)
os.chmod(os.path.join('/sbin/', file_name), stat.S_IEXEC)
class CentOS(RH):

View File

@ -30,9 +30,6 @@ from octavia.common import constants as consts
CONF = cfg.CONF
ETH_X_VIP_CONF = 'plug_vip_ethX.conf.j2'
ETH_X_PORT_CONF = 'plug_port_ethX.conf.j2'
LOG = logging.getLogger(__name__)
@ -40,26 +37,20 @@ class Plug(object):
def __init__(self, osutils):
self._osutils = osutils
def plug_lo(self):
self._osutils.write_interface_file(
interface="lo",
ip_address="127.0.0.1",
prefixlen=8)
def plug_vip(self, vip, subnet_cidr, gateway,
mac_address, mtu=None, vrrp_ip=None, host_routes=None):
# Validate vip and subnet_cidr, calculate broadcast address and netmask
try:
render_host_routes = []
ip = ipaddress.ip_address(vip)
network = ipaddress.ip_network(subnet_cidr)
vip = ip.exploded
broadcast = network.broadcast_address.exploded
netmask = (network.prefixlen if ip.version == 6
else network.netmask.exploded)
vrrp_version = None
if vrrp_ip:
vrrp_ip_obj = ipaddress.ip_address(vrrp_ip)
vrrp_version = vrrp_ip_obj.version
if host_routes:
for hr in host_routes:
network = ipaddress.ip_network(hr['destination'])
render_host_routes.append({'network': network,
'gw': hr['nexthop']})
prefixlen = network.prefixlen
except ValueError:
return webob.Response(json=dict(message="Invalid VIP"),
status=400)
@ -76,27 +67,16 @@ class Plug(object):
# Always put the VIP interface as eth1
primary_interface = consts.NETNS_PRIMARY_INTERFACE
secondary_interface = "{interface}:0".format(
interface=primary_interface)
interface_file_path = self._osutils.get_network_interface_file(
primary_interface)
self._osutils.create_netns_dir()
self._osutils.write_interfaces_file()
self._osutils.write_vip_interface_file(
interface_file_path=interface_file_path,
primary_interface=primary_interface,
interface=primary_interface,
vip=vip,
ip=ip,
broadcast=broadcast,
netmask=netmask,
ip_version=ip.version,
prefixlen=prefixlen,
gateway=gateway,
mtu=mtu,
vrrp_ip=vrrp_ip,
vrrp_version=vrrp_version,
render_host_routes=render_host_routes)
host_routes=host_routes)
# Update the list of interfaces to add to the namespace
# This is used in the amphora reboot case to re-establish the namespace
@ -136,13 +116,8 @@ class Plug(object):
ipr.link('set', index=idx, net_ns_fd=consts.AMPHORA_NAMESPACE,
IFLA_IFNAME=primary_interface)
# In an ha amphora, keepalived should bring the VIP interface up
if (CONF.controller_worker.loadbalancer_topology ==
consts.TOPOLOGY_ACTIVE_STANDBY):
secondary_interface = None
# bring interfaces up
self._osutils.bring_interfaces_up(
ip, primary_interface, secondary_interface)
self._osutils.bring_interfaces_up(ip, primary_interface)
return webob.Response(json=dict(
message="OK",
@ -189,13 +164,10 @@ class Plug(object):
LOG.info('Plugged interface %s will become %s in the namespace %s',
default_netns_interface, netns_interface,
consts.AMPHORA_NAMESPACE)
interface_file_path = self._osutils.get_network_interface_file(
netns_interface)
self._osutils.write_port_interface_file(
netns_interface=netns_interface,
interface=netns_interface,
fixed_ips=fixed_ips,
mtu=mtu,
interface_file_path=interface_file_path)
mtu=mtu)
# Update the list of interfaces to add to the namespace
self._update_plugged_interfaces_file(netns_interface, mac_address)

View File

@ -64,6 +64,8 @@ class Server(object):
register_app_error_handler(self.app)
self._plug.plug_lo()
self.app.add_url_rule(rule='/', view_func=self.version_discovery,
methods=['GET'])
self.app.add_url_rule(rule=PATH_PREFIX +

View File

@ -25,12 +25,4 @@ ExecStart=-/bin/sh -c '/usr/bin/sort -k 1 /var/lib/octavia/plugged_interfaces >
# Assign the interfaces into the namespace with the appropriate name
ExecStart=-/bin/sh -c '/sbin/ip link | awk \'{getline n; print $0,n}\' | awk \'{sub(":","",$2)} { for(i=1;i<=NF;i++) if ($i == "link/ether") {print $(i+1) " " $2} }\' | sort -k 1 | join -j 1 - /var/lib/octavia/plugged_interfaces.sorted | awk \'{system("ip link set "$2" netns {{ amphora_nsname }} name "$3"")}\''
# Bring up all of the namespace interfaces
{%- if HasIFUPAll %}
# Ubuntu seems to not correctly set up the lo iface when calling ifup -a
# Disable it first, before setting it up.
ExecStart=-/sbin/ip netns exec {{ amphora_nsname }} ifdown lo
ExecStart=-/sbin/ip netns exec {{ amphora_nsname }} ifup -a
{%- else %}
ExecStart=-/sbin/ip netns exec {{ amphora_nsname }} ifup lo
ExecStart=-/bin/awk '{system("/sbin/ip netns exec {{ amphora_nsname }} ifup " $2)}' /var/lib/octavia/plugged_interfaces
{%- endif %}
ExecStart=-/sbin/ip netns exec {{ amphora_nsname }} amphora-interface up all

View File

@ -1,43 +0,0 @@
{#
# Copyright 2015 Hewlett-Packard Development Company, L.P.
#
# 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.
#}
# Generated by Octavia agent
auto {{ interface }}
{%- if ip_address %}
iface {{ interface }} inet{{ '6' if ipv6 }} static
address {{ ip_address }}
broadcast {{ broadcast }}
netmask {{ netmask }}
{%- if mtu %}
mtu {{ mtu }}
{%- endif %}
{%- for hr in host_routes %}
{%- if ((hr.network.version == 4 and hr.network.prefixlen == 32) or
(hr.network.version == 6 and hr.network.prefixlen == 128)) %}
up route add -host {{ hr.network }} gw {{ hr.gw }} dev {{ interface }}
down route del -host {{ hr.network }} gw {{ hr.gw }} dev {{ interface }}
{%- else %}
up route add -net {{ hr.network }} gw {{ hr.gw }} dev {{ interface }}
down route del -net {{ hr.network }} gw {{ hr.gw }} dev {{ interface }}
{%- endif %}
{%- endfor %}
post-up /usr/local/bin/lvs-masquerade.sh add {{ 'ipv6' if ipv6 else 'ipv4' }} {{ interface }}
post-down /usr/local/bin/lvs-masquerade.sh delete {{ 'ipv6' if ipv6 else 'ipv4' }} {{ interface }}
{%- else %}
iface {{ interface }} inet dhcp
auto {{ interface }}:0
iface {{ interface }}:0 inet6 auto
{%- endif %}

View File

@ -1,82 +0,0 @@
{#
# Copyright 2015 Hewlett-Packard Development Company, L.P.
# Copyright 2016 Rackspace
#
# 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.
#}
# Generated by Octavia agent
{%- if topology == consts.TOPOLOGY_SINGLE %}
auto {{ interface }} {{ interface }}:0
{%- else %}
auto {{ interface }}
{%- endif %}
{%- if vrrp_ip %}
iface {{ interface }} inet{{ '6' if vrrp_ipv6 }} static
address {{ vrrp_ip }}
broadcast {{ broadcast }}
netmask {{ netmask }}
{%- if gateway %}
gateway {{ gateway }}
{%- endif %}
{%- if mtu %}
mtu {{ mtu }}
{%- endif %}
{%- for hr in host_routes %}
{%- if ((hr.network.version == 4 and hr.network.prefixlen == 32) or
(hr.network.version == 6 and hr.network.prefixlen == 128)) %}
up route add -host {{ hr.network }} gw {{ hr.gw }} dev {{ interface }}
down route del -host {{ hr.network }} gw {{ hr.gw }} dev {{ interface }}
{%- else %}
up route add -net {{ hr.network }} gw {{ hr.gw }} dev {{ interface }}
down route del -net {{ hr.network }} gw {{ hr.gw }} dev {{ interface }}
{%- endif %}
{%- endfor %}
{%- else %}
iface {{ interface }} inet{{ '6' if vip_ipv6 }} {{ 'auto' if vip_ipv6 else 'dhcp' }}
{%- endif %}
{%- if topology == consts.TOPOLOGY_SINGLE %}
iface {{ interface }}:0 inet{{ '6' if vip_ipv6 }} static
address {{ vip }}
broadcast {{ broadcast }}
netmask {{ netmask }}
{%- endif %}
# Add a source routing table to allow members to access the VIP
{%- if gateway %}
post-up /sbin/ip {{ '-6 ' if vip_ipv6 }}route add default via {{ gateway }} dev {{ interface }} onlink table 1
post-down /sbin/ip {{ '-6 ' if vip_ipv6 }}route del default via {{ gateway }} dev {{ interface }} onlink table 1
{# Keepalived will insert and remove this route in active/standby #}
{%- if topology == consts.TOPOLOGY_SINGLE %}
post-up /sbin/ip {{ '-6 ' if vip_ipv6 }}route add {{ network }} dev {{ interface }} src {{ vip }} scope link table 1
post-down /sbin/ip {{ '-6 ' if vip_ipv6 }}route del {{ network }} dev {{ interface }} src {{ vip }} scope link table 1
{%- endif %}
{%- endif %}
{%- for hr in host_routes %}
post-up /sbin/ip {{ '-6 ' if vip_ipv6 }}route add {{ hr.network }} via {{ hr.gw }} dev {{ interface }} onlink table 1
post-down /sbin/ip {{ '-6 ' if vip_ipv6 }}route del {{ hr.network }} via {{ hr.gw }} dev {{ interface }} onlink table 1
{%- endfor %}
{# Keepalived will insert and remove this rule in active/standby #}
{%- if topology == consts.TOPOLOGY_SINGLE %}
post-up /sbin/ip {{ '-6 ' if vip_ipv6 }}rule add from {{ vip }}/{{ '128' if vip_ipv6 else '32' }} table 1 priority 100
post-down /sbin/ip {{ '-6 ' if vip_ipv6 }}rule del from {{ vip }}/{{ '128' if vip_ipv6 else '32' }} table 1 priority 100
{%- endif %}
post-up /usr/local/bin/lvs-masquerade.sh add {{ 'ipv6' if vip_ipv6 else 'ipv4' }} {{ interface }}
post-down /usr/local/bin/lvs-masquerade.sh delete {{ 'ipv6' if vip_ipv6 else 'ipv4' }} {{ interface }}

View File

@ -1,47 +0,0 @@
{#
# Copyright 2017 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.
#}
# Generated by Octavia agent
NM_CONTROLLED="no"
DEVICE="{{ interface }}"
ONBOOT="yes"
TYPE="Ethernet"
USERCTL="yes"
{%- if ipv6 %}
IPV6INIT="yes"
{%- if mtu %}
IPV6_MTU="{{ mtu }}"
{%- endif %}
{%- if ip_address %}
IPV6_AUTOCONF="no"
IPV6ADDR="{{ ip_address }}"
{%- else %}
IPV6_AUTOCONF="yes"
{%- endif %}
{%- else %}
IPV6INIT="no"
{%- if mtu %}
MTU="{{ mtu }}"
{%- endif %}
{%- if ip_address %}
BOOTPROTO="static"
IPADDR="{{ ip_address }}"
NETMASK="{{ netmask }}"
{%- else %}
BOOTPROTO="dhcp"
PERSISTENT_DHCLIENT="1"
{%- endif %}
{%- endif %}

View File

@ -1,19 +0,0 @@
{# 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.
#}
# Generated by Octavia agent
#!/bin/bash
if [[ "$1" != "lo" ]]
then
/usr/local/bin/lvs-masquerade.sh delete ipv4 $1
/usr/local/bin/lvs-masquerade.sh delete ipv6 $1
fi

View File

@ -1,19 +0,0 @@
{# 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.
#}
# Generated by Octavia agent
#!/bin/bash
if [[ "$1" != "lo" ]]
then
/usr/local/bin/lvs-masquerade.sh add ipv4 $1
/usr/local/bin/lvs-masquerade.sh add ipv6 $1
fi

View File

@ -1,60 +0,0 @@
{#
# Copyright 2017 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.
#}
# Generated by Octavia agent
NM_CONTROLLED="no"
DEVICE="{{ interface }}"
ONBOOT="yes"
TYPE="Ethernet"
USERCTL="yes"
{%- if vrrp_ip %}
{%- if vrrp_ipv6 %}
IPV6INIT="yes"
IPV6_DEFROUTE="yes"
IPV6_AUTOCONF="no"
IPV6ADDR="{{ vrrp_ip }}/{{ prefix }}"
{%- if gateway %}
IPV6_DEFAULTGW="{{ gateway }}"
{%- endif %}
{%- if mtu %}
IPV6_MTU="{{ mtu }}"
{%- endif %}
{%- else %} {# not vrrp_ipv6 #}
BOOTPROTO="static"
IPADDR="{{ vrrp_ip }}"
NETMASK="{{ netmask }}"
{%- if gateway %}
GATEWAY="{{ gateway }}"
{%- endif %}
MTU="{{ mtu }}"
{%- endif %} {# end if vrrp_ipv6 #}
{%- else %} {# not vrrp_ip #}
{%- if vip_ipv6 %}
IPV6INIT="yes"
IPV6_DEFROUTE="yes"
IPV6_AUTOCONF="yes"
{%- else %}
BOOTPROTO="dhcp"
PERSISTENT_DHCLIENT="1"
{%- endif %} {# end if vip_ipv6 #}
{%- endif %} {# end if vrrp_ip #}
{%- if topology == consts.TOPOLOGY_SINGLE -%}
{%- if vip_ipv6 %}
IPV6ADDR_SECONDARIES="{{ vip }}/{{ prefix }}"
{%- endif %}
{%- endif %}

View File

@ -1,29 +0,0 @@
{#
# Copyright 2017 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.
#}
# Generated by Octavia agent
NM_CONTROLLED="no"
DEVICE="{{ interface }}:0"
NAME="{{ interface }}:0"
ONBOOT="yes"
ARPCHECK="no"
IPV6INIT="no"
{%- if mtu %}
MTU="{{ mtu }}"
{%- endif %}
BOOTPROTO="static"
IPADDR="{{ vip }}"
NETMASK="{{ netmask }}"

View File

@ -1,29 +0,0 @@
{#
# Copyright 2017 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.
#}
# Generated by Octavia agent
{%- for hr in host_routes %}
{{ hr.network }} via {{ hr.gw }} dev {{ interface }}
{%- endfor %}
# Add a source routing table to allow members to access the VIP
{%- if gateway %}
{%- if topology == consts.TOPOLOGY_SINGLE %}
{{ network }} dev {{ interface }} src {{ vip }} scope link table 1
{%- endif %}
default table 1 via {{ gateway }} dev {{ interface }}
{%- endif %}
{%- for hr in host_routes %}
{{ hr.network }} table 1 via {{ hr.gw }} dev {{ interface }}
{%- endfor %}

View File

@ -1,17 +0,0 @@
{#
# Copyright 2017 Rackspace, US Inc.
#
# 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.
#}
# Generated by Octavia agent
from {{ vip }} table 1

View File

@ -62,11 +62,7 @@ haproxy_start()
# Assign the interfaces into the namespace with the appropriate name
ip link | awk '{getline n; print $0,n}' | awk '{sub(":","",$2)} { for(i=1;i<=NF;i++) if ($i == "link/ether") {print $(i+1) " " $2} }' | sort -k 1 | join -j 1 - /var/lib/octavia/plugged_interfaces.sorted | awk '{system("ip link set "$2" netns {{ amphora_nsname }} name "$3"")}' || true
# Bring up all of the namespace interfaces
{%- if HasIFUPAll %}
ip netns exec {{ amphora_nsname }} ifup -a || true
{%- else %}
awk '{system("/sbin/ip netns exec {{ amphora_nsname }} ifup " $2)}' /var/lib/octavia/plugged_interfaces || true
{%- endif %}
ip netns exec {{ amphora_nsname }} amphora-interface up all || true
start-stop-daemon --start --pidfile "$PIDFILE" \
--exec $HAPROXY -- -f "$CONFIG" -f "$USER_GROUP_CONF_PATH" -L "$PEER_NAME" -D -p "$PIDFILE" \

View File

@ -42,11 +42,7 @@ pre-start script
# Assign the interfaces into the namespace with the appropriate name
ip link | awk '{getline n; print $0,n}' | awk '{sub(":","",$2)} { for(i=1;i<=NF;i++) if ($i == "link/ether") {print $(i+1) " " $2} }' | sort -k 1 | join -j 1 - /var/lib/octavia/plugged_interfaces.sorted | awk '{system("ip link set "$2" netns {{ amphora_nsname }} name "$3"")}' || true
# Bring up all of the namespace interfaces
{%- if HasIFUPAll %}
ip netns exec {{ amphora_nsname }} ifup -a || true
{%- else %}
awk '{system("/sbin/ip netns exec {{ amphora_nsname }} ifup " $2)}' /var/lib/octavia/plugged_interfaces || true
{%- endif %}
ip netns exec {{ amphora_nsname }} amphora-interface up all || true
end script

View File

@ -22,7 +22,6 @@ import jinja2
from oslo_config import cfg
from oslo_log import log as logging
from octavia.amphorae.backends.agent.api_server import osutils
from octavia.amphorae.backends.utils import ip_advertisement
from octavia.amphorae.backends.utils import network_utils
from octavia.common import constants as consts
@ -241,8 +240,6 @@ def get_os_init_system():
def install_netns_systemd_service():
os_utils = osutils.BaseOS.get_os_util()
flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC
# mode 00644
mode = (stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH)
@ -261,8 +258,7 @@ def install_netns_systemd_service():
with os.fdopen(os.open(netns_path, flags, mode), 'w') as text_file:
text = jinja_env.get_template(
consts.AMP_NETNS_SVC_PREFIX + '.systemd.j2').render(
amphora_nsname=consts.AMPHORA_NAMESPACE,
HasIFUPAll=os_utils.has_ifup_all())
amphora_nsname=consts.AMPHORA_NAMESPACE)
text_file.write(text)

View File

@ -40,9 +40,6 @@ agent_server_cert = {{ agent_server_cert }}
{% if agent_server_network_dir -%}
agent_server_network_dir = {{ agent_server_network_dir }}
{% endif -%}
{% if agent_server_network_file -%}
agent_server_network_file = {{ agent_server_network_file }}
{% endif -%}
agent_request_read_timeout = {{ agent_request_read_timeout }}
amphora_id = {{ amphora_id }}
amphora_udp_driver = {{ amphora_udp_driver }}

View File

@ -0,0 +1,237 @@
# Copyright 2020 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 errno
import ipaddress
import os
import socket
import subprocess
import time
from oslo_config import cfg
from oslo_log import log as logging
import pyroute2
from octavia.amphorae.backends.utils import interface_file
from octavia.common import constants as consts
from octavia.common import exceptions
CONF = cfg.CONF
LOG = logging.getLogger(__name__)
class InterfaceController(object):
ADD = 'add'
DELETE = 'delete'
SET = 'set'
def interface_file_list(self):
net_dir = interface_file.InterfaceFile.get_directory()
for f in os.listdir(net_dir):
for ext in interface_file.InterfaceFile.get_extensions():
if f.endswith(ext):
yield os.path.join(net_dir, f)
def list(self):
interfaces = {}
for f in self.interface_file_list():
iface = interface_file.InterfaceFile.from_file(f)
interfaces[iface.name] = iface
return interfaces
def _family(self, address):
return (socket.AF_INET6
if ipaddress.ip_network(address, strict=False).version == 6
else socket.AF_INET)
def _ipr_command(self, method, command,
retry_on_invalid_argument=False,
retry_interval=.2,
raise_on_error=True,
max_retries=20,
**kwargs):
for dummy in range(max_retries + 1):
try:
method(command, **kwargs)
break
except pyroute2.NetlinkError as e:
if e.code == errno.EINVAL and retry_on_invalid_argument:
LOG.debug("Retrying after %d sec.", retry_interval)
time.sleep(retry_interval)
continue
if command == self.ADD and e.code != errno.EEXIST:
msg = "Cannot call {} {} (with {}): {}".format(
method.__name__, command, kwargs, e)
if raise_on_error:
raise exceptions.AmphoraNetworkConfigException(msg)
LOG.error(msg)
return
else:
msg = "Cannot call {} {} (with {}) after {} retries.".format(
method.__name__, command, kwargs, max_retries)
if raise_on_error:
raise exceptions.AmphoraNetworkConfigException(msg)
LOG.error(msg)
def _dhclient_up(self, interface_name):
cmd = ["/sbin/dhclient",
"-lf",
"/var/lib/dhclient/dhclient-{}.leases".format(
interface_name),
"-pf",
"/run/dhclient-{}.pid".format(interface_name),
interface_name]
LOG.debug("Running '%s'", cmd)
subprocess.check_output(cmd, stderr=subprocess.STDOUT)
def _dhclient_down(self, interface_name):
cmd = ["/sbin/dhclient",
"-r",
"-lf",
"/var/lib/dhclient/dhclient-{}.leases".format(
interface_name),
"-pf",
"/run/dhclient-{}.pid".format(interface_name),
interface_name]
LOG.debug("Running '%s'", cmd)
subprocess.check_output(cmd, stderr=subprocess.STDOUT)
def _ipv6auto_up(self, interface_name):
# Set values to enable SLAAC on interface_name
# accept_ra is set to 2 to accept router advertisements if forwarding
# is enabled on the interface
for key, value in (('accept_ra', 2),
('autoconf', 1)):
cmd = ["/sbin/sysctl",
"-w",
"net.ipv6.conf.{}.{}={}".format(interface_name,
key, value)]
LOG.debug("Running '%s'", cmd)
subprocess.check_output(cmd, stderr=subprocess.STDOUT)
def _ipv6auto_down(self, interface_name):
for key, value in (('accept_ra', 0),
('autoconf', 0)):
cmd = ["/sbin/sysctl",
"-w",
"net.ipv6.conf.{}.{}={}".format(interface_name,
key, value)]
LOG.debug("Running '%s'", cmd)
subprocess.check_output(cmd, stderr=subprocess.STDOUT)
def up(self, interface):
LOG.info("Setting interface %s up", interface.name)
for address in interface.addresses:
if address.get(consts.DHCP):
self._dhclient_up(interface.name)
if address.get(consts.IPV6AUTO):
self._ipv6auto_up(interface.name)
with pyroute2.IPRoute() as ipr:
idx = ipr.link_lookup(ifname=interface.name)[0]
self._ipr_command(ipr.link, self.SET, index=idx,
state=consts.IFACE_UP, mtu=interface.mtu)
for address in interface.addresses:
if (consts.ADDRESS not in address or
address.get(consts.DHCP) or
address.get(consts.IPV6AUTO)):
continue
address[consts.FAMILY] = self._family(address[consts.ADDRESS])
LOG.debug("%s: Adding address %s", interface.name, address)
self._ipr_command(ipr.addr, self.ADD, index=idx, **address)
for route in interface.routes:
route[consts.FAMILY] = self._family(route[consts.DST])
LOG.debug("%s: Adding route %s", interface.name, route)
# Set retry_on_invalid_argument=True because the interface
# might not be ready after setting its addresses
# Note: can we use 'replace' instead of 'add' here?
# Set raise_on_error to False, possible invalid (user-defined)
# routes from the subnet's host_routes will not break the
# script.
self._ipr_command(ipr.route, self.ADD,
retry_on_invalid_argument=True,
raise_on_error=False,
oif=idx, **route)
for rule in interface.rules:
rule[consts.FAMILY] = self._family(rule[consts.SRC])
LOG.debug("%s: Adding rule %s", interface.name, rule)
self._ipr_command(ipr.rule, self.ADD,
retry_on_invalid_argument=True,
**rule)
for script in interface.scripts[consts.IFACE_UP]:
LOG.debug("%s: Running command '%s'",
interface.name, script[consts.COMMAND])
subprocess.check_output(script[consts.COMMAND].split())
def down(self, interface):
LOG.info("Setting interface %s down", interface.name)
for address in interface.addresses:
if address.get(consts.DHCP):
self._dhclient_down(interface.name)
if address.get(consts.IPV6AUTO):
self._ipv6auto_down(interface.name)
with pyroute2.IPRoute() as ipr:
idx = ipr.link_lookup(ifname=interface.name)[0]
link = ipr.get_links(idx)[0]
current_state = link.get(consts.STATE)
if current_state == consts.IFACE_UP:
for rule in interface.rules:
rule[consts.FAMILY] = self._family(rule[consts.SRC])
LOG.debug("%s: Deleting rule %s", interface.name, rule)
self._ipr_command(ipr.rule, self.DELETE,
raise_on_error=False, **rule)
for route in interface.routes:
route[consts.FAMILY] = self._family(route[consts.DST])
LOG.debug("%s: Deleting route %s", interface.name, route)
self._ipr_command(ipr.route, self.DELETE,
raise_on_error=False, oif=idx, **route)
for address in interface.addresses:
if consts.ADDRESS not in address:
continue
address[consts.FAMILY] = self._family(
address[consts.ADDRESS])
LOG.debug("%s: Deleting address %s",
interface.name, address)
self._ipr_command(ipr.addr, self.DELETE,
raise_on_error=False,
index=idx, **address)
self._ipr_command(ipr.link, self.SET, raise_on_error=False,
index=idx, state=consts.IFACE_DOWN)
if current_state == consts.IFACE_UP:
for script in interface.scripts[consts.IFACE_DOWN]:
LOG.debug("%s: Running command '%s'",
interface.name, script[consts.COMMAND])
try:
subprocess.check_output(script[consts.COMMAND].split())
except Exception as e:
LOG.error("Error while running command '%s' on %s: %s",
script[consts.COMMAND], interface.name, e)

View File

@ -0,0 +1,218 @@
# Copyright 2020 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 ipaddress
import os
import stat
from oslo_config import cfg
import simplejson
from octavia.common import constants as consts
CONF = cfg.CONF
class InterfaceFile(object):
def __init__(self, name, mtu=None, addresses=None,
routes=None, rules=None, scripts=None):
self.name = name
self.mtu = mtu
self.addresses = addresses or []
self.routes = routes or []
self.rules = rules or []
self.scripts = scripts or {
consts.IFACE_UP: [],
consts.IFACE_DOWN: []
}
@classmethod
def get_extensions(cls):
return [".json"]
@classmethod
def load(cls, fp):
return simplejson.load(fp)
@classmethod
def dump(cls, obj):
return simplejson.dumps(obj)
@classmethod
def from_file(cls, filename):
with open(filename, encoding='utf-8') as fp:
config = cls.load(fp)
return InterfaceFile(**config)
@classmethod
def get_directory(cls):
return (CONF.amphora_agent.agent_server_network_dir or
consts.AMP_NET_DIR_TEMPLATE)
@classmethod
def get_host_routes(cls, routes, **kwargs):
host_routes = []
if routes:
for hr in routes:
route = {
consts.DST: hr['destination'],
consts.GATEWAY: hr['nexthop'],
consts.FLAGS: [consts.ONLINK]
}
route.update(kwargs)
host_routes.append(route)
return host_routes
def write(self):
mode = stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH
flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC
net_dir = self.get_directory()
try:
os.makedirs(net_dir)
except OSError:
pass
interface_file = "{}.json".format(self.name)
with os.fdopen(os.open(os.path.join(net_dir, interface_file),
flags, mode), 'w') as fp:
interface = {
consts.NAME: self.name,
consts.ADDRESSES: self.addresses,
consts.ROUTES: self.routes,
consts.RULES: self.rules,
consts.SCRIPTS: self.scripts
}
if self.mtu:
interface[consts.MTU] = self.mtu
fp.write(self.dump(interface))
class VIPInterfaceFile(InterfaceFile):
def __init__(self, name, mtu,
vip, ip_version, prefixlen,
gateway, vrrp_ip, host_routes,
topology):
super().__init__(name, mtu=mtu)
if vrrp_ip:
self.addresses.append({
consts.ADDRESS: vrrp_ip,
consts.PREFIXLEN: prefixlen
})
else:
key = consts.DHCP if ip_version == 4 else consts.IPV6AUTO
self.addresses.append({
key: True
})
if gateway:
# Add default routes if there's a gateway
self.routes.append({
consts.DST: (
"::/0" if ip_version == 6 else "0.0.0.0/0"),
consts.GATEWAY: gateway,
consts.FLAGS: [consts.ONLINK]
})
self.routes.append({
consts.DST: (
"::/0" if ip_version == 6 else "0.0.0.0/0"),
consts.GATEWAY: gateway,
consts.FLAGS: [consts.ONLINK],
consts.TABLE: 1,
})
# In ACTIVE_STANDBY topology, keepalived sets these addresses, routes
# and rules
if topology == consts.TOPOLOGY_SINGLE:
self.addresses.append({
consts.ADDRESS: vip,
consts.PREFIXLEN: prefixlen
})
vip_cidr = ipaddress.ip_network(
"{}/{}".format(vip, prefixlen), strict=False)
self.routes.append({
consts.DST: vip_cidr.exploded,
consts.PREFSRC: vip,
consts.SCOPE: 'link',
consts.TABLE: 1,
})
self.rules.append({
consts.SRC: vip,
consts.SRC_LEN: 128 if ip_version == 6 else 32,
consts.TABLE: 1,
})
self.routes.extend(self.get_host_routes(host_routes))
self.routes.extend(self.get_host_routes(host_routes,
table=1))
self.scripts[consts.IFACE_UP].append({
consts.COMMAND: (
"/usr/local/bin/lvs-masquerade.sh add {} {}".format(
'ipv6' if ip_version == 6 else 'ipv4', name))
})
self.scripts[consts.IFACE_DOWN].append({
consts.COMMAND: (
"/usr/local/bin/lvs-masquerade.sh delete {} {}".format(
'ipv6' if ip_version == 6 else 'ipv4', name))
})
class PortInterfaceFile(InterfaceFile):
def __init__(self, name, mtu, fixed_ips):
super().__init__(name, mtu=mtu)
if fixed_ips:
ip_versions = set()
for fixed_ip in fixed_ips:
ip_addr = fixed_ip['ip_address']
cidr = fixed_ip['subnet_cidr']
ip = ipaddress.ip_address(ip_addr)
network = ipaddress.ip_network(cidr)
prefixlen = network.prefixlen
self.addresses.append({
consts.ADDRESS: fixed_ip['ip_address'],
consts.PREFIXLEN: prefixlen,
})
ip_versions.add(ip.version)
host_routes = self.get_host_routes(
fixed_ip.get('host_routes', []))
self.routes.extend(host_routes)
else:
ip_versions = {4, 6}
self.addresses.append({
consts.DHCP: True,
consts.IPV6AUTO: True
})
for ip_version in ip_versions:
self.scripts[consts.IFACE_UP].append({
consts.COMMAND: (
"/usr/local/bin/lvs-masquerade.sh add {} {}".format(
'ipv6' if ip_version == 6 else 'ipv4', name))
})
self.scripts[consts.IFACE_DOWN].append({
consts.COMMAND: (
"/usr/local/bin/lvs-masquerade.sh delete {} {}".format(
'ipv6' if ip_version == 6 else 'ipv4', name))
})

90
octavia/cmd/interface.py Normal file
View File

@ -0,0 +1,90 @@
# Copyright 2020 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 sys
from oslo_config import cfg
from octavia.amphorae.backends.utils import interface
from octavia.common import config
from octavia.common import exceptions
class InterfaceException(exceptions.OctaviaException):
message = "Could not configure interface: %(msg)s"
def interfaces_find(interface_controller, name):
all_interfaces = interface_controller.list()
if name == "all":
return all_interfaces.values()
if name in all_interfaces:
return [all_interfaces[name]]
msg = "Could not find interface '{}'.".format(name)
raise InterfaceException(msg=msg)
def interfaces_update(interfaces, action_fn, action_str):
errors = []
for iface in interfaces:
try:
action_fn(iface)
except Exception as e:
errors.append("Error on action '{}' for interface {}: {}.".format(
action_str, iface.name, e))
if errors:
raise InterfaceException(msg=", ".join(errors))
def interface_cmd(interface_name, action):
interface_controller = interface.InterfaceController()
if action == "up":
action_fn = interface_controller.up
elif action == "down":
action_fn = interface_controller.down
else:
raise InterfaceException(
msg="Unknown action '{}'".format(action))
interfaces = interfaces_find(interface_controller,
interface_name)
interfaces_update(interfaces, action_fn, action)
def main():
config.init(sys.argv[1:-2])
config.setup_logging(cfg.CONF)
try:
action = sys.argv[-2]
interface_name = sys.argv[-1]
except IndexError:
print("usage: {} [up|down] <interface>".format(sys.argv[0]))
sys.exit(1)
try:
interface_cmd(interface_name, action)
except Exception as e:
print("Error: {}".format(e))
sys.exit(2)
if __name__ == "__main__":
main()

View File

@ -160,7 +160,11 @@ amphora_agent_opts = [
cfg.StrOpt('agent_server_network_file',
help=_("The file where the network interfaces are located. "
"Specifying this will override any value set for "
"agent_server_network_dir.")),
"agent_server_network_dir."),
deprecated_for_removal=True,
deprecated_reason=_('New amphora interface management '
'does not support single interface file.'),
deprecated_since='Xena'),
cfg.IntOpt('agent_request_read_timeout', default=180,
help=_("The time in seconds to allow a request from the "
"controller to run before terminating the socket.")),

View File

@ -713,8 +713,6 @@ NOAUTH = 'noauth'
TESTING = 'testing'
# Amphora distro-specific data
UBUNTU_AMP_NET_DIR_TEMPLATE = '/etc/netns/{netns}/network/interfaces.d/'
RH_AMP_NET_DIR_TEMPLATE = '/etc/netns/{netns}/sysconfig/network-scripts/'
UBUNTU = 'ubuntu'
CENTOS = 'centos'
@ -894,3 +892,38 @@ SUPPORTED_ALPN_PROTOCOLS = [lib_consts.ALPN_PROTOCOL_HTTP_2,
AMPHORA_SUPPORTED_ALPN_PROTOCOLS = [lib_consts.ALPN_PROTOCOL_HTTP_2,
lib_consts.ALPN_PROTOCOL_HTTP_1_1,
lib_consts.ALPN_PROTOCOL_HTTP_1_0]
# Amphora interface fields
MTU = 'mtu'
ADDRESSES = 'addresses'
ROUTES = 'routes'
RULES = 'rules'
SCRIPTS = 'scripts'
# pyroute2 fields
STATE = 'state'
FAMILY = 'family'
ADDRESS = 'address'
PREFIXLEN = 'prefixlen'
DHCP = 'dhcp'
IPV6AUTO = 'ipv6auto'
DST = 'dst'
PREFSRC = 'prefsrc'
GATEWAY = 'gateway'
FLAGS = 'flags'
ONLINK = 'onlink'
TABLE = 'table'
SCOPE = 'scope'
SRC = 'src'
SRC_LEN = 'src_len'
IFACE_UP = 'up'
IFACE_DOWN = 'down'
COMMAND = 'command'
# Amphora network directory
AMP_NET_DIR_TEMPLATE = '/etc/octavia/interfaces/'

View File

@ -414,3 +414,8 @@ class NetworkServiceError(OctaviaException):
class InvalidIPAddress(APIException):
msg = _('The IP Address %(ip_addr)s is invalid.')
code = 400
class AmphoraNetworkConfigException(OctaviaException):
message = _('Cannot configure network resource in the amphora: '
'%(detail)s')

View File

@ -12,10 +12,13 @@
# 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 ipaddress
from unittest import mock
import fixtures
from octavia.common import constants as consts
# Borrowed from neutron
# https://review.opendev.org/#/c/232716/
@ -41,3 +44,67 @@ class OpenFixture(fixtures.Fixture):
self._patch = mock.patch('builtins.open', new=replacement_open)
self._patch.start()
self.addCleanup(self._patch.stop)
def assert_address_lists_equal(obj, l1, l2):
obj.assertEqual(len(l1), len(l2),
"Address lists don't match: {} vs {}".format(l1, l2))
for a1, a2 in zip(l1, l2):
if consts.ADDRESS in a1 and consts.ADDRESS in a2:
obj.assertEqual(
ipaddress.ip_address(a1[consts.ADDRESS]),
ipaddress.ip_address(a2[consts.ADDRESS]))
obj.assertEqual(a1[consts.PREFIXLEN],
a2[consts.PREFIXLEN])
else:
obj.assertEqual(a1, a2)
def assert_route_lists_equal(obj, l1, l2):
obj.assertEqual(len(l1), len(l2),
"Routes don't match: {} vs {}".format(l1, l2))
for r1, r2 in zip(l1, l2):
obj.assertEqual(
ipaddress.ip_network(r1[consts.DST]),
ipaddress.ip_network(r2[consts.DST]))
if consts.GATEWAY in r1 and consts.GATEWAY in r2:
obj.assertEqual(
ipaddress.ip_address(r1[consts.GATEWAY]),
ipaddress.ip_address(r2[consts.GATEWAY]))
if consts.PREFSRC in r1 and consts.PREFSRC in r2:
obj.assertEqual(
ipaddress.ip_address(r1[consts.PREFSRC]),
ipaddress.ip_address(r2[consts.PREFSRC]))
for attr in (consts.ONLINK, consts.TABLE, consts.SCOPE):
obj.assertEqual(r1.get(attr), r2.get(attr))
def assert_rule_lists_equal(obj, l1, l2):
obj.assertEqual(len(l1), len(l2))
for r1, r2 in zip(l1, l2):
obj.assertEqual(
ipaddress.ip_address(r1[consts.SRC]),
ipaddress.ip_address(r2[consts.SRC]))
obj.assertEqual(r1[consts.SRC_LEN], r2[consts.SRC_LEN])
obj.assertEqual(r1[consts.TABLE], r2[consts.TABLE])
def assert_script_lists_equal(obj, l1, l2):
obj.assertEqual(l1, l2)
def assert_interface_files_equal(obj, i1, i2):
obj.assertEqual(i1[consts.NAME], i2[consts.NAME])
obj.assertEqual(i1.get(consts.MTU), i2.get(consts.MTU))
assert_address_lists_equal(obj,
i1[consts.ADDRESSES],
i2[consts.ADDRESSES])
assert_route_lists_equal(obj,
i1[consts.ROUTES],
i2[consts.ROUTES])
assert_rule_lists_equal(obj,
i1[consts.RULES],
i2[consts.RULES])
assert_script_lists_equal(obj,
i1[consts.SCRIPTS],
i2[consts.SCRIPTS])

View File

@ -12,18 +12,10 @@
# License for the specific language governing permissions and limitations
# under the License.
import ipaddress
import os
import shutil
from unittest import mock
from oslo_config import fixture as oslo_fixture
from octavia.amphorae.backends.agent.api_server import osutils
from octavia.common import config
from octavia.common import constants as consts
from octavia.common import exceptions as octavia_exceptions
from octavia.common import utils
from octavia.tests.common import utils as test_utils
from octavia.tests.unit import base
@ -73,204 +65,6 @@ class TestOSUtils(base.TestCase):
octavia_exceptions.InvalidAmphoraOperatingSystem,
osutils.BaseOS.get_os_util)
def test_get_network_interface_file(self):
conf = self.useFixture(oslo_fixture.Config(config.cfg.CONF))
fake_agent_server_network_dir = "/path/to/interface"
fake_agent_server_network_file = "/path/to/interfaces_file"
base_fake_nic_path = os.path.join(fake_agent_server_network_dir,
consts.NETNS_PRIMARY_INTERFACE)
base_real_nic_path = os.path.join(
consts.UBUNTU_AMP_NET_DIR_TEMPLATE.format(
netns=consts.AMPHORA_NAMESPACE),
consts.NETNS_PRIMARY_INTERFACE)
rh_interface_name = 'ifcfg-{nic}'.format(
nic=consts.NETNS_PRIMARY_INTERFACE)
rh_fake_nic_path = os.path.join(fake_agent_server_network_dir,
rh_interface_name)
rh_real_nic_path = os.path.join(
consts.RH_AMP_NET_DIR_TEMPLATE.format(
netns=consts.AMPHORA_NAMESPACE),
rh_interface_name)
ubuntu_interface_name = '{nic}.cfg'.format(
nic=consts.NETNS_PRIMARY_INTERFACE)
ubuntu_fake_nic_path = os.path.join(fake_agent_server_network_dir,
ubuntu_interface_name)
ubuntu_real_nic_path = os.path.join(
consts.UBUNTU_AMP_NET_DIR_TEMPLATE.format(
netns=consts.AMPHORA_NAMESPACE),
ubuntu_interface_name)
# Check that agent_server_network_file is returned, when provided
conf.config(group="amphora_agent",
agent_server_network_file=fake_agent_server_network_file)
base_interface_file = (
self.base_os_util.
get_network_interface_file(consts.NETNS_PRIMARY_INTERFACE))
self.assertEqual(fake_agent_server_network_file, base_interface_file)
rh_interface_file = (
self.rh_os_util.
get_network_interface_file(consts.NETNS_PRIMARY_INTERFACE))
self.assertEqual(fake_agent_server_network_file, rh_interface_file)
ubuntu_interface_file = (
self.ubuntu_os_util.
get_network_interface_file(consts.NETNS_PRIMARY_INTERFACE))
self.assertEqual(fake_agent_server_network_file, ubuntu_interface_file)
# Check that agent_server_network_dir is used, when provided
conf.config(group="amphora_agent", agent_server_network_file=None)
conf.config(group="amphora_agent",
agent_server_network_dir=fake_agent_server_network_dir)
base_interface_file = (
self.base_os_util.
get_network_interface_file(consts.NETNS_PRIMARY_INTERFACE))
self.assertEqual(base_fake_nic_path, base_interface_file)
rh_interface_file = (
self.rh_os_util.
get_network_interface_file(consts.NETNS_PRIMARY_INTERFACE))
self.assertEqual(rh_fake_nic_path, rh_interface_file)
ubuntu_interface_file = (
self.ubuntu_os_util.
get_network_interface_file(consts.NETNS_PRIMARY_INTERFACE))
self.assertEqual(ubuntu_fake_nic_path, ubuntu_interface_file)
# Check When neither agent_server_network_dir or
# agent_server_network_file where provided.
conf.config(group="amphora_agent", agent_server_network_file=None)
conf.config(group="amphora_agent", agent_server_network_dir=None)
base_interface_file = (
self.base_os_util.
get_network_interface_file(consts.NETNS_PRIMARY_INTERFACE))
self.assertEqual(base_real_nic_path, base_interface_file)
rh_interface_file = (
self.rh_os_util.
get_network_interface_file(consts.NETNS_PRIMARY_INTERFACE))
self.assertEqual(rh_real_nic_path, rh_interface_file)
ubuntu_interface_file = (
self.ubuntu_os_util.
get_network_interface_file(consts.NETNS_PRIMARY_INTERFACE))
self.assertEqual(ubuntu_real_nic_path, ubuntu_interface_file)
def _test_RH_get_static_routes_interface_file(self, version):
conf = self.useFixture(oslo_fixture.Config(config.cfg.CONF))
fake_agent_server_network_dir = "/path/to/interface"
fake_agent_server_network_file = "/path/to/interfaces_file"
route = 'route6' if version == 6 else 'route'
rh_route_name = '{route}-{nic}'.format(
route=route, nic=consts.NETNS_PRIMARY_INTERFACE)
rh_fake_route_path = os.path.join(fake_agent_server_network_dir,
rh_route_name)
rh_real_route_path = os.path.join(
consts.RH_AMP_NET_DIR_TEMPLATE.format(
netns=consts.AMPHORA_NAMESPACE),
rh_route_name)
# Check that agent_server_network_file is returned, when provided
conf.config(group="amphora_agent",
agent_server_network_file=fake_agent_server_network_file)
rh_route_file = (
self.rh_os_util.
get_static_routes_interface_file(consts.NETNS_PRIMARY_INTERFACE,
version))
self.assertEqual(fake_agent_server_network_file, rh_route_file)
# Check that agent_server_network_dir is used, when provided
conf.config(group="amphora_agent", agent_server_network_file=None)
conf.config(group="amphora_agent",
agent_server_network_dir=fake_agent_server_network_dir)
rh_route_file = (
self.rh_os_util.
get_static_routes_interface_file(consts.NETNS_PRIMARY_INTERFACE,
version))
self.assertEqual(rh_fake_route_path, rh_route_file)
# Check When neither agent_server_network_dir or
# agent_server_network_file where provided.
conf.config(group="amphora_agent", agent_server_network_file=None)
conf.config(group="amphora_agent", agent_server_network_dir=None)
rh_route_file = (
self.rh_os_util.
get_static_routes_interface_file(consts.NETNS_PRIMARY_INTERFACE,
version))
self.assertEqual(rh_real_route_path, rh_route_file)
def test_RH_get_static_routes_interface_file(self):
self._test_RH_get_static_routes_interface_file(4)
def test_RH_get_static_routes_interface_file_ipv6(self):
self._test_RH_get_static_routes_interface_file(6)
def _test_RH_get_route_rules_interface_file(self, version):
conf = self.useFixture(oslo_fixture.Config(config.cfg.CONF))
fake_agent_server_network_dir = "/path/to/interface"
fake_agent_server_network_file = "/path/to/interfaces_file"
rule = 'rule6' if version == 6 else 'rule'
rh_route_rules_name = '{rule}-{nic}'.format(
rule=rule, nic=consts.NETNS_PRIMARY_INTERFACE)
rh_fake_route_rules_path = os.path.join(fake_agent_server_network_dir,
rh_route_rules_name)
rh_real_route_rules_path = os.path.join(
consts.RH_AMP_NET_DIR_TEMPLATE.format(
netns=consts.AMPHORA_NAMESPACE),
rh_route_rules_name)
# Check that agent_server_network_file is returned, when provided
conf.config(group="amphora_agent",
agent_server_network_file=fake_agent_server_network_file)
rh_route_rules_file = (
self.rh_os_util.
get_route_rules_interface_file(consts.NETNS_PRIMARY_INTERFACE,
version))
self.assertEqual(fake_agent_server_network_file, rh_route_rules_file)
# Check that agent_server_network_dir is used, when provided
conf.config(group="amphora_agent", agent_server_network_file=None)
conf.config(group="amphora_agent",
agent_server_network_dir=fake_agent_server_network_dir)
rh_route_rules_file = (
self.rh_os_util.
get_route_rules_interface_file(consts.NETNS_PRIMARY_INTERFACE,
version))
self.assertEqual(rh_fake_route_rules_path, rh_route_rules_file)
# Check When neither agent_server_network_dir or
# agent_server_network_file where provided.
conf.config(group="amphora_agent", agent_server_network_file=None)
conf.config(group="amphora_agent", agent_server_network_dir=None)
rh_route_rules_file = (
self.rh_os_util.
get_route_rules_interface_file(consts.NETNS_PRIMARY_INTERFACE,
version))
self.assertEqual(rh_real_route_rules_path, rh_route_rules_file)
def test_RH_get_route_rules_interface_file(self):
self._test_RH_get_route_rules_interface_file(4)
def test_RH_get_route_rules_interface_file_ipv6(self):
self._test_RH_get_route_rules_interface_file(6)
def test_cmd_get_version_of_installed_package(self):
package_name = 'foo'
ubuntu_cmd = "dpkg-query -W -f=${{Version}} {name}".format(
@ -302,12 +96,9 @@ class TestOSUtils(base.TestCase):
package_name))
self.assertEqual(centos_cmd, returned_centos_cmd)
def test_has_ifup_all(self):
self.assertTrue(self.base_os_util.has_ifup_all())
self.assertTrue(self.ubuntu_os_util.has_ifup_all())
self.assertFalse(self.rh_os_util.has_ifup_all())
def test_write_vip_interface_file(self):
@mock.patch('octavia.amphorae.backends.utils.interface_file.'
'VIPInterfaceFile')
def test_write_vip_interface_file(self, mock_vip_interface_file):
netns_interface = u'eth1234'
FIXED_IP = u'192.0.2.2'
SUBNET_CIDR = u'192.0.2.0/24'
@ -323,94 +114,64 @@ class TestOSUtils(base.TestCase):
ip = ipaddress.ip_address(FIXED_IP)
network = ipaddress.ip_network(SUBNET_CIDR)
broadcast = network.broadcast_address.exploded
netmask = network.netmask.exploded
netmask_prefix = utils.netmask_to_prefix(netmask)
ipv6 = ipaddress.ip_address(FIXED_IP_IPV6)
networkv6 = ipaddress.ip_network(SUBNET_CIDR_IPV6)
broadcastv6 = networkv6.broadcast_address.exploded
netmaskv6 = networkv6.prefixlen
host_routes = [
{'gw': NEXTHOP, 'network': ipaddress.ip_network(DEST1)},
{'gw': NEXTHOP, 'network': ipaddress.ip_network(DEST2)}
{'nexthop': NEXTHOP, 'destination': DEST1},
{'nexthop': NEXTHOP, 'destination': DEST2}
]
path = self.ubuntu_os_util.get_network_interface_file(netns_interface)
mock_open = self.useFixture(test_utils.OpenFixture(path)).mock_open
mock_template = mock.MagicMock()
# Test an IPv4 VIP
with mock.patch('os.open'), mock.patch.object(
os, 'fdopen', mock_open):
self.ubuntu_os_util.write_vip_interface_file(
interface_file_path=path,
primary_interface=netns_interface,
vip=FIXED_IP,
ip=ip,
broadcast=broadcast,
netmask=netmask,
gateway=GATEWAY,
mtu=MTU,
vrrp_ip=None,
vrrp_version=None,
render_host_routes=host_routes,
template_vip=mock_template)
mock_template.render.assert_called_once_with(
consts=consts,
interface=netns_interface,
vip=FIXED_IP,
vip_ipv6=False,
prefix=netmask_prefix,
broadcast=broadcast,
netmask=netmask,
ip_version=ip.version,
prefixlen=network.prefixlen,
gateway=GATEWAY,
mtu=MTU,
vrrp_ip=None,
host_routes=host_routes)
mock_vip_interface_file.assert_called_once_with(
name=netns_interface,
vip=FIXED_IP,
ip_version=ip.version,
prefixlen=network.prefixlen,
gateway=GATEWAY,
network=SUBNET_CIDR,
mtu=MTU,
vrrp_ip=None,
vrrp_ipv6=False,
host_routes=host_routes,
topology="SINGLE",
)
topology="SINGLE")
mock_vip_interface_file.return_value.write.assert_called_once()
# Now test with an IPv6 VIP
mock_template.reset_mock()
with mock.patch('os.open'), mock.patch.object(
os, 'fdopen', mock_open):
self.ubuntu_os_util.write_vip_interface_file(
interface_file_path=path,
primary_interface=netns_interface,
vip=FIXED_IP_IPV6,
ip=ipv6,
broadcast=broadcastv6,
netmask=netmaskv6,
gateway=GATEWAY,
mtu=MTU,
vrrp_ip=None,
vrrp_version=None,
render_host_routes=host_routes,
template_vip=mock_template)
mock_vip_interface_file.reset_mock()
mock_template.render.assert_called_once_with(
consts=consts,
self.ubuntu_os_util.write_vip_interface_file(
interface=netns_interface,
vip=FIXED_IP_IPV6,
vip_ipv6=True,
prefix=netmaskv6,
broadcast=broadcastv6,
netmask=netmaskv6,
ip_version=ipv6.version,
prefixlen=networkv6.prefixlen,
gateway=GATEWAY,
network=SUBNET_CIDR_IPV6,
mtu=MTU,
vrrp_ip=None,
vrrp_ipv6=False,
host_routes=host_routes,
topology="SINGLE",
)
host_routes=host_routes)
def test_write_port_interface_file(self):
mock_vip_interface_file.assert_called_once_with(
name=netns_interface,
vip=FIXED_IP_IPV6,
ip_version=ipv6.version,
prefixlen=networkv6.prefixlen,
gateway=GATEWAY,
mtu=MTU,
vrrp_ip=None,
host_routes=host_routes,
topology="SINGLE")
@mock.patch('octavia.amphorae.backends.utils.interface_file.'
'PortInterfaceFile')
def test_write_port_interface_file(self, mock_port_interface_file):
FIXED_IP = u'192.0.2.2'
NEXTHOP = u'192.0.2.1'
DEST = u'198.51.100.0/24'
@ -431,111 +192,14 @@ class TestOSUtils(base.TestCase):
netns_interface = 'eth1234'
MTU = 1450
fixed_ips = [ip_addr, ipv6_addr]
path = 'mypath'
mock_template = mock.MagicMock()
mock_open = self.useFixture(test_utils.OpenFixture(path)).mock_open
mock_gen_text = mock.MagicMock()
mock_local_scripts = mock.MagicMock()
mock_wr_fi = mock.MagicMock()
with mock.patch('os.open'), mock.patch.object(
os, 'fdopen', mock_open), mock.patch.object(
osutils.BaseOS, '_generate_network_file_text', mock_gen_text):
self.base_os_util.write_port_interface_file(
netns_interface=netns_interface,
interface=netns_interface,
fixed_ips=fixed_ips,
mtu=MTU,
interface_file_path=path,
template_port=mock_template)
mtu=MTU)
mock_gen_text.assert_called_once_with(
netns_interface, fixed_ips, MTU, mock_template)
mock_gen_text.reset_mock()
with mock.patch('os.open'), mock.patch.object(
os, 'fdopen', mock_open), mock.patch.object(
osutils.BaseOS, '_generate_network_file_text',
mock_gen_text), mock.patch.object(
osutils.RH, '_write_ifup_ifdown_local_scripts_if_possible',
mock_local_scripts), mock.patch.object(
osutils.RH, 'write_static_routes_interface_file', mock_wr_fi):
self.rh_os_util.write_port_interface_file(
netns_interface=netns_interface,
mock_port_interface_file.assert_called_once_with(
name=netns_interface,
fixed_ips=fixed_ips,
mtu=MTU,
interface_file_path=path,
template_port=mock_template)
rh_route_name = 'route-{nic}'.format(nic=netns_interface)
rh_real_route_path = os.path.join(
consts.RH_AMP_NET_DIR_TEMPLATE.format(
netns=consts.AMPHORA_NAMESPACE),
rh_route_name)
rh_route_name_ipv6 = 'route6-{nic}'.format(nic=netns_interface)
rh_real_route_path_ipv6 = os.path.join(
consts.RH_AMP_NET_DIR_TEMPLATE.format(
netns=consts.AMPHORA_NAMESPACE),
rh_route_name_ipv6)
exp_routes = [
{'network': ipaddress.ip_network(DEST), 'gw': NEXTHOP}
]
exp_routes_ipv6 = [
{'network': ipaddress.ip_network(DEST_IPV6), 'gw': NEXTHOP_IPV6}
]
expected_calls = [
mock.call(rh_real_route_path, netns_interface,
exp_routes, mock.ANY, None, None, None),
mock.call(rh_real_route_path_ipv6, netns_interface,
exp_routes_ipv6, mock.ANY, None, None, None)]
mock_gen_text.assert_called_once_with(
netns_interface, fixed_ips, MTU, mock_template)
self.assertEqual(2, mock_wr_fi.call_count)
mock_wr_fi.assert_has_calls(expected_calls)
mock_local_scripts.assert_called_once()
@mock.patch('shutil.copy2')
@mock.patch('os.makedirs')
@mock.patch('shutil.copytree')
def test_create_netns_dir(self, mock_copytree, mock_makedirs, mock_copy2):
network_dir = 'foo'
netns_network_dir = 'fake_netns_network'
ignore = shutil.ignore_patterns('fake_eth*', 'fake_loopback*')
self.rh_os_util.create_netns_dir(network_dir,
netns_network_dir,
ignore)
mock_copytree.assert_any_call(
network_dir,
os.path.join('/etc/netns/',
consts.AMPHORA_NAMESPACE,
netns_network_dir),
ignore=ignore,
symlinks=True)
mock_makedirs.assert_any_call(os.path.join('/etc/netns/',
consts.AMPHORA_NAMESPACE))
mock_copy2.assert_any_call(
'/etc/sysconfig/network',
'/etc/netns/{netns}/sysconfig'.format(
netns=consts.AMPHORA_NAMESPACE))
mock_copytree.reset_mock()
mock_makedirs.reset_mock()
mock_copy2.reset_mock()
self.ubuntu_os_util.create_netns_dir(network_dir,
netns_network_dir,
ignore)
mock_copytree.assert_any_call(
network_dir,
os.path.join('/etc/netns/',
consts.AMPHORA_NAMESPACE,
netns_network_dir),
ignore=ignore,
symlinks=True)
mock_makedirs.assert_any_call(os.path.join('/etc/netns/',
consts.AMPHORA_NAMESPACE))
mock_copy2.assert_not_called()
mtu=MTU)
mock_port_interface_file.return_value.write.assert_called_once()

View File

@ -206,142 +206,3 @@ class TestPlug(base.TestCase):
# Interface is not found in netns
self.assertFalse(self.test_plug._netns_interface_exists('321'))
class TestPlugNetwork(base.TestCase):
def setUp(self):
super().setUp()
self.mock_platform = mock.patch("distro.id").start()
def __generate_network_file_text_static_ip(self):
netns_interface = 'eth1234'
FIXED_IP = '192.0.2.2'
BROADCAST = '192.0.2.255'
SUBNET_CIDR = '192.0.2.0/24'
NETMASK = '255.255.255.0'
DEST1 = '198.51.100.0/24'
DEST2 = '203.0.113.0/24'
NEXTHOP = '192.0.2.1'
MTU = 1450
fixed_ips = [{'ip_address': FIXED_IP,
'subnet_cidr': SUBNET_CIDR,
'host_routes': [
{'destination': DEST1, 'nexthop': NEXTHOP},
{'destination': DEST2, 'nexthop': NEXTHOP}
]}]
format_text = (
'\n\n# Generated by Octavia agent\n'
'auto {netns_interface}\n'
'iface {netns_interface} inet static\n'
'address {fixed_ip}\n'
'broadcast {broadcast}\n'
'netmask {netmask}\n'
'mtu {mtu}\n'
'up route add -net {dest1} gw {nexthop} dev {netns_interface}\n'
'down route del -net {dest1} gw {nexthop} dev {netns_interface}\n'
'up route add -net {dest2} gw {nexthop} dev {netns_interface}\n'
'down route del -net {dest2} gw {nexthop} dev {netns_interface}\n'
'post-up /usr/local/bin/lvs-masquerade.sh add ipv4 eth1234\n'
'post-down /usr/local/bin/lvs-masquerade.sh delete ipv4 eth1234\n')
template_port = osutils.j2_env.get_template('plug_port_ethX.conf.j2')
text = self.test_plug._osutils._generate_network_file_text(
netns_interface, fixed_ips, MTU, template_port)
expected_text = format_text.format(netns_interface=netns_interface,
fixed_ip=FIXED_IP,
broadcast=BROADCAST,
netmask=NETMASK,
mtu=MTU,
dest1=DEST1,
dest2=DEST2,
nexthop=NEXTHOP)
self.assertEqual(expected_text, text)
def __generate_network_file_text_two_static_ips(self):
netns_interface = 'eth1234'
FIXED_IP = '192.0.2.2'
BROADCAST = '192.0.2.255'
SUBNET_CIDR = '192.0.2.0/24'
NETMASK = '255.255.255.0'
DEST1 = '198.51.100.0/24'
DEST2 = '203.0.113.0/24'
NEXTHOP = '192.0.2.1'
MTU = 1450
FIXED_IP_IPV6 = '2001:0db8:0000:0000:0000:0000:0000:0001'
BROADCAST_IPV6 = '2001:0db8:ffff:ffff:ffff:ffff:ffff:ffff'
SUBNET_CIDR_IPV6 = '2001:db8::/32'
NETMASK_IPV6 = '32'
fixed_ips = [{'ip_address': FIXED_IP,
'subnet_cidr': SUBNET_CIDR,
'host_routes': [
{'destination': DEST1, 'nexthop': NEXTHOP},
{'destination': DEST2, 'nexthop': NEXTHOP}
]},
{'ip_address': FIXED_IP_IPV6,
'subnet_cidr': SUBNET_CIDR_IPV6,
'host_routes': []}
]
format_text = (
'\n\n# Generated by Octavia agent\n'
'auto {netns_interface}\n'
'iface {netns_interface} inet static\n'
'address {fixed_ip}\n'
'broadcast {broadcast}\n'
'netmask {netmask}\n'
'mtu {mtu}\n'
'up route add -net {dest1} gw {nexthop} dev {netns_interface}\n'
'down route del -net {dest1} gw {nexthop} dev {netns_interface}\n'
'up route add -net {dest2} gw {nexthop} dev {netns_interface}\n'
'down route del -net {dest2} gw {nexthop} dev {netns_interface}\n'
'post-up /usr/local/bin/lvs-masquerade.sh add ipv4 '
'{netns_interface}\n'
'post-down /usr/local/bin/lvs-masquerade.sh delete ipv4 '
'{netns_interface}\n'
'\n\n# Generated by Octavia agent\n'
'auto {netns_interface}\n'
'iface {netns_interface} inet6 static\n'
'address {fixed_ip_ipv6}\n'
'broadcast {broadcast_ipv6}\n'
'netmask {netmask_ipv6}\n'
'mtu {mtu}\n'
'post-up /usr/local/bin/lvs-masquerade.sh add ipv6 '
'{netns_interface}\n'
'post-down /usr/local/bin/lvs-masquerade.sh delete ipv6 '
'{netns_interface}\n')
template_port = osutils.j2_env.get_template('plug_port_ethX.conf.j2')
text = self.test_plug._osutils._generate_network_file_text(
netns_interface, fixed_ips, MTU, template_port)
expected_text = format_text.format(netns_interface=netns_interface,
fixed_ip=FIXED_IP,
broadcast=BROADCAST,
netmask=NETMASK,
mtu=MTU,
dest1=DEST1,
dest2=DEST2,
nexthop=NEXTHOP,
fixed_ip_ipv6=FIXED_IP_IPV6,
broadcast_ipv6=BROADCAST_IPV6,
netmask_ipv6=NETMASK_IPV6)
self.assertEqual(expected_text, text)
def _setup(self, os):
self.mock_platform.return_value = os
self.osutil = osutils.BaseOS.get_os_util()
self.test_plug = plug.Plug(self.osutil)
def test__generate_network_file_text_static_ip_ubuntu(self):
self._setup("ubuntu")
self.__generate_network_file_text_static_ip()
def test__generate_network_file_text_static_ip_centos(self):
self._setup("centos")
self.__generate_network_file_text_static_ip()
def test__generate_network_file_text_two_static_ips_ubuntu(self):
self._setup("ubuntu")
self.__generate_network_file_text_two_static_ips()
def test__generate_network_file_text_two_static_ips_centos(self):
self._setup("centos")
self.__generate_network_file_text_two_static_ips()

View File

@ -147,7 +147,7 @@ class TestUtil(base.TestCase):
mock_jinja_env.get_template.assert_called_once_with(
consts.AMP_NETNS_SVC_PREFIX + '.systemd.j2')
mock_template.render.assert_called_once_with(
amphora_nsname=consts.AMPHORA_NAMESPACE, HasIFUPAll=True)
amphora_nsname=consts.AMPHORA_NAMESPACE)
handle = m()
handle.write.assert_called_with('script')

View File

@ -55,8 +55,6 @@ class AgentJinjaTestCase(base.TestCase):
def test_build_agent_config(self):
ajc = agent_jinja_cfg.AgentJinjaTemplater()
# Test execution order could influence this with the test below
self.conf.config(group='amphora_agent',
agent_server_network_file=None)
self.conf.config(group="amphora_agent",
administrative_log_facility=1)
self.conf.config(group="amphora_agent", user_log_facility=0)
@ -98,8 +96,6 @@ class AgentJinjaTestCase(base.TestCase):
def test_build_agent_config_with_interfaces_file(self):
ajc = agent_jinja_cfg.AgentJinjaTemplater()
self.conf.config(group="amphora_agent",
agent_server_network_file='/etc/network/interfaces')
self.conf.config(group="haproxy_amphora", use_upstart='False')
self.conf.config(group="amphora_agent",
administrative_log_facility=1)
@ -130,8 +126,6 @@ class AgentJinjaTestCase(base.TestCase):
'/etc/octavia/certs/server.pem\n'
'agent_server_network_dir = '
'/etc/network/interfaces.d/\n'
'agent_server_network_file = '
'/etc/network/interfaces\n'
'agent_request_read_timeout = 180\n'
'amphora_id = ' + AMP_ID + '\n'
'amphora_udp_driver = keepalived_lvs\n'
@ -145,8 +139,6 @@ class AgentJinjaTestCase(base.TestCase):
def test_build_agent_config_with_new_udp_driver(self):
ajc = agent_jinja_cfg.AgentJinjaTemplater()
self.conf.config(group='amphora_agent',
agent_server_network_file=None)
self.conf.config(group="amphora_agent",
amphora_udp_driver='new_udp_driver')
self.conf.config(group="amphora_agent",

View File

@ -0,0 +1,902 @@
# Copyright 2020 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 errno
import os
import socket
from unittest import mock
import pyroute2
from octavia.amphorae.backends.utils import interface
from octavia.amphorae.backends.utils import interface_file
from octavia.common import constants as consts
from octavia.common import exceptions
from octavia.tests.common import utils as test_utils
import octavia.tests.unit.base as base
class TestInterface(base.TestCase):
@mock.patch('os.listdir')
@mock.patch('octavia.amphorae.backends.utils.interface_file.'
'InterfaceFile.get_directory')
def test_interface_file_list(self, mock_get_directory, mock_listdir):
mock_get_directory.return_value = consts.AMP_NET_DIR_TEMPLATE
ifaces = ('eth0', 'eth7', 'eth8')
mock_listdir.return_value = [
"{}.json".format(iface)
for iface in ifaces
]
mock_listdir.return_value.extend(["invalidfile"])
controller = interface.InterfaceController()
r = controller.interface_file_list()
config_file_list = list(r)
for iface in ifaces:
f = os.path.join(consts.AMP_NET_DIR_TEMPLATE,
"{}.json".format(iface))
self.assertIn(f, config_file_list)
# unsupported file
f = os.path.join(consts.AMP_NET_DIR_TEMPLATE,
"invalidfile")
self.assertNotIn(f, config_file_list)
# non existing file
f = os.path.join(consts.AMP_NET_DIR_TEMPLATE,
"eth2.json")
self.assertNotIn(f, config_file_list)
@mock.patch('os.listdir')
@mock.patch('octavia.amphorae.backends.utils.interface_file.'
'InterfaceFile.get_directory')
def test_list(self, mock_get_directory, mock_listdir):
mock_get_directory.return_value = consts.AMP_NET_DIR_TEMPLATE
mock_listdir.return_value = ["fakeiface.json"]
content = ('{\n'
'"addresses": [\n'
'{"address": "10.0.0.2",\n'
'"prefixlen": 24}\n'
'],\n'
'"mtu": 1450,\n'
'"name": "eth1",\n'
'"routes": [\n'
'{"dst": "0.0.0.0/0",\n'
'"gateway": "10.0.0.1"},\n'
'{"dst": "10.11.0.0/16",\n'
'"gateway": "10.0.0.24"}\n'
'],\n'
'"rules": [\n'
'{"src": "10.0.0.2",\n'
'"src_len": 32,\n'
'"table": 100}\n'
'],\n'
'"scripts": {\n'
'"up": [\n'
'{"command": "up-script"}],\n'
'"down": [\n'
'{"command": "down-script"}]\n'
'}}\n')
filename = os.path.join(consts.AMP_NET_DIR_TEMPLATE,
"fakeiface.json")
self.useFixture(
test_utils.OpenFixture(filename,
contents=content))
controller = interface.InterfaceController()
ifaces = controller.list()
self.assertIn("eth1", ifaces)
iface = ifaces["eth1"]
expected_dict = {
consts.NAME: "eth1",
consts.MTU: 1450,
consts.ADDRESSES: [{
consts.ADDRESS: "10.0.0.2",
consts.PREFIXLEN: 24
}],
consts.ROUTES: [{
consts.DST: "0.0.0.0/0",
consts.GATEWAY: "10.0.0.1"
}, {
consts.DST: "10.11.0.0/16",
consts.GATEWAY: "10.0.0.24"
}],
consts.RULES: [{
consts.SRC: "10.0.0.2",
consts.SRC_LEN: 32,
consts.TABLE: 100
}],
consts.SCRIPTS: {
consts.IFACE_UP: [{
consts.COMMAND: "up-script"
}],
consts.IFACE_DOWN: [{
consts.COMMAND: "down-script"
}]
}
}
self.assertEqual(expected_dict[consts.NAME], iface.name)
self.assertEqual(expected_dict[consts.MTU], iface.mtu)
test_utils.assert_address_lists_equal(
self, expected_dict[consts.ADDRESSES], iface.addresses)
test_utils.assert_rule_lists_equal(
self, expected_dict[consts.RULES], iface.rules)
test_utils.assert_script_lists_equal(
self, expected_dict[consts.SCRIPTS], iface.scripts)
def test__ipr_command(self):
mock_ipr_addr = mock.MagicMock()
controller = interface.InterfaceController()
controller._ipr_command(mock_ipr_addr,
controller.ADD,
arg1=1, arg2=2)
mock_ipr_addr.assert_called_once_with('add', arg1=1, arg2=2)
def test__ipr_command_add_eexist(self):
mock_ipr_addr = mock.MagicMock()
mock_ipr_addr.side_effect = [
pyroute2.NetlinkError(code=errno.EEXIST)
]
controller = interface.InterfaceController()
controller._ipr_command(mock_ipr_addr,
controller.ADD,
arg1=1, arg2=2)
mock_ipr_addr.assert_called_once_with('add', arg1=1, arg2=2)
def test__ipr_command_add_retry(self):
mock_ipr_addr = mock.MagicMock()
mock_ipr_addr.side_effect = [
pyroute2.NetlinkError(code=errno.EINVAL),
pyroute2.NetlinkError(code=errno.EINVAL),
pyroute2.NetlinkError(code=errno.EINVAL),
None
]
controller = interface.InterfaceController()
controller._ipr_command(mock_ipr_addr,
controller.ADD,
retry_on_invalid_argument=True,
retry_interval=0,
arg1=1, arg2=2)
mock_ipr_addr.assert_has_calls([
mock.call('add', arg1=1, arg2=2),
mock.call('add', arg1=1, arg2=2),
mock.call('add', arg1=1, arg2=2),
mock.call('add', arg1=1, arg2=2)])
def test__ipr_command_add_einval_failed(self):
mock_ipr_addr = mock.MagicMock()
mock_ipr_addr.__name__ = "addr"
mock_ipr_addr.side_effect = [
pyroute2.NetlinkError(code=errno.EINVAL)
] * 21
controller = interface.InterfaceController()
self.assertRaises(exceptions.AmphoraNetworkConfigException,
controller._ipr_command,
mock_ipr_addr,
controller.ADD,
retry_on_invalid_argument=True,
max_retries=20,
retry_interval=0,
arg1=1, arg2=2)
mock_ipr_addr.assert_has_calls([
mock.call('add', arg1=1, arg2=2)
] * 20)
def test__ipr_command_add_failed(self):
mock_ipr_addr = mock.MagicMock()
mock_ipr_addr.__name__ = "addr"
mock_ipr_addr.side_effect = [
pyroute2.NetlinkError(code=errno.ENOENT)
]
controller = interface.InterfaceController()
self.assertRaises(exceptions.AmphoraNetworkConfigException,
controller._ipr_command,
mock_ipr_addr,
controller.ADD,
retry_on_invalid_argument=True,
max_retries=20,
retry_interval=0,
arg1=1, arg2=2)
mock_ipr_addr.assert_called_once_with(
'add', arg1=1, arg2=2)
def test__ipr_command_delete_failed_no_raise(self):
mock_ipr_addr = mock.MagicMock()
mock_ipr_addr.__name__ = "addr"
mock_ipr_addr.side_effect = [
pyroute2.NetlinkError(code=errno.EINVAL)
]
controller = interface.InterfaceController()
controller._ipr_command(mock_ipr_addr,
controller.DELETE,
retry_on_invalid_argument=True,
max_retries=0,
raise_on_error=False,
arg1=1, arg2=2)
mock_ipr_addr.assert_called_once_with(
'delete', arg1=1, arg2=2)
def test__ipr_command_add_failed_retry_no_raise(self):
mock_ipr_addr = mock.MagicMock()
mock_ipr_addr.__name__ = "addr"
mock_ipr_addr.side_effect = [
pyroute2.NetlinkError(code=errno.ENOENT)
]
controller = interface.InterfaceController()
controller._ipr_command(mock_ipr_addr,
controller.ADD,
max_retries=20,
retry_interval=0,
raise_on_error=False,
arg1=1, arg2=2)
mock_ipr_addr.assert_called_once_with(
'add', arg1=1, arg2=2)
@mock.patch('subprocess.check_output')
def test__dhclient_up(self, mock_check_output):
iface = "iface2"
controller = interface.InterfaceController()
controller._dhclient_up(iface)
mock_check_output.assert_called_once_with(
["/sbin/dhclient",
"-lf",
"/var/lib/dhclient/dhclient-{}.leases".format(
iface),
"-pf",
"/run/dhclient-{}.pid".format(iface),
iface], stderr=-2)
@mock.patch('subprocess.check_output')
def test__dhclient_down(self, mock_check_output):
iface = "iface2"
controller = interface.InterfaceController()
controller._dhclient_down(iface)
mock_check_output.assert_called_once_with(
["/sbin/dhclient",
"-r",
"-lf",
"/var/lib/dhclient/dhclient-{}.leases".format(
iface),
"-pf",
"/run/dhclient-{}.pid".format(iface),
iface], stderr=-2)
@mock.patch('subprocess.check_output')
def test__ipv6auto_up(self, mock_check_output):
iface = "iface2"
controller = interface.InterfaceController()
controller._ipv6auto_up(iface)
mock_check_output.assert_has_calls([
mock.call(["/sbin/sysctl", "-w",
"net.ipv6.conf.iface2.accept_ra=2"], stderr=-2),
mock.call(["/sbin/sysctl", "-w",
"net.ipv6.conf.iface2.autoconf=1"], stderr=-2)])
@mock.patch('subprocess.check_output')
def test__ipv6auto_down(self, mock_check_output):
iface = "iface2"
controller = interface.InterfaceController()
controller._ipv6auto_down(iface)
mock_check_output.assert_has_calls([
mock.call(["/sbin/sysctl", "-w",
"net.ipv6.conf.iface2.accept_ra=0"], stderr=-2),
mock.call(["/sbin/sysctl", "-w",
"net.ipv6.conf.iface2.autoconf=0"], stderr=-2)])
@mock.patch('pyroute2.IPRoute.rule')
@mock.patch('pyroute2.IPRoute.route')
@mock.patch('pyroute2.IPRoute.addr')
@mock.patch('pyroute2.IPRoute.link')
@mock.patch('pyroute2.IPRoute.link_lookup')
@mock.patch('subprocess.check_output')
def test_up(self, mock_check_output, mock_link_lookup, mock_link,
mock_addr, mock_route, mock_rule):
iface = interface_file.InterfaceFile(
name="eth1",
mtu=1450,
addresses=[{
consts.ADDRESS: '1.2.3.4',
consts.PREFIXLEN: 24
}, {
consts.ADDRESS: '10.2.3.4',
consts.PREFIXLEN: 16
}, {
consts.ADDRESS: '2001:db8::3',
consts.PREFIXLEN: 64
}],
routes=[{
consts.DST: '10.0.0.0/8',
consts.GATEWAY: '1.0.0.1',
consts.TABLE: 10,
consts.ONLINK: True
}, {
consts.DST: '20.0.0.0/8',
consts.GATEWAY: '1.0.0.2',
consts.PREFSRC: '1.2.3.4',
consts.SCOPE: 'link'
}, {
consts.DST: '2001:db8:2::1/128',
consts.GATEWAY: '2001:db8::1'
}],
rules=[{
consts.SRC: '1.1.1.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 eth1"
}],
consts.IFACE_DOWN: [{
consts.COMMAND: "post-down eth1"
}],
})
idx = mock.MagicMock()
mock_link_lookup.return_value = [idx]
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.call(controller.ADD,
index=idx,
address='10.2.3.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='10.0.0.0/8',
gateway='1.0.0.1',
table=10,
onlink=True,
family=socket.AF_INET),
mock.call(controller.ADD,
oif=idx,
dst='20.0.0.0/8',
gateway='1.0.0.2',
prefsrc='1.2.3.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="1.1.1.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(["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.link_lookup')
@mock.patch('subprocess.check_output')
def test_up_auto(self, mock_check_output, mock_link_lookup, mock_link,
mock_addr, mock_route, mock_rule):
iface = interface_file.InterfaceFile(
name="eth1",
mtu=1450,
addresses=[{
consts.DHCP: True,
consts.IPV6AUTO: True
}],
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]
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_not_called()
mock_route.assert_not_called()
mock_rule.assert_not_called()
mock_check_output.assert_has_calls([
mock.call(["/sbin/dhclient",
"-lf",
"/var/lib/dhclient/dhclient-{}.leases".format(
iface.name),
"-pf",
"/run/dhclient-{}.pid".format(iface.name),
iface.name], stderr=-2),
mock.call(["/sbin/sysctl", "-w",
"net.ipv6.conf.{}.accept_ra=2".format(iface.name)],
stderr=-2),
mock.call(["/sbin/sysctl", "-w",
"net.ipv6.conf.{}.autoconf=1".format(iface.name)],
stderr=-2),
mock.call(["post-up", iface.name])
])
@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_down(self, mock_check_output, mock_link_lookup, mock_get_links,
mock_link, mock_addr, mock_route, mock_rule):
iface = interface_file.InterfaceFile(
name="eth1",
mtu=1450,
addresses=[{
consts.ADDRESS: '1.2.3.4',
consts.PREFIXLEN: 24
}, {
consts.ADDRESS: '10.2.3.4',
consts.PREFIXLEN: 16
}, {
consts.ADDRESS: '2001:db8::3',
consts.PREFIXLEN: 64
}],
routes=[{
consts.DST: '10.0.0.0/8',
consts.GATEWAY: '1.0.0.1',
consts.TABLE: 10,
consts.ONLINK: True
}, {
consts.DST: '20.0.0.0/8',
consts.GATEWAY: '1.0.0.2',
consts.PREFSRC: '1.2.3.4',
consts.SCOPE: 'link'
}, {
consts.DST: '2001:db8:2::1/128',
consts.GATEWAY: '2001:db8::1'
}],
rules=[{
consts.SRC: '1.1.1.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 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_UP
}]
controller = interface.InterfaceController()
controller.down(iface)
mock_link.assert_called_once_with(
controller.SET,
index=idx,
state=consts.IFACE_DOWN)
mock_addr.assert_has_calls([
mock.call(controller.DELETE,
index=idx,
address='1.2.3.4',
prefixlen=24,
family=socket.AF_INET),
mock.call(controller.DELETE,
index=idx,
address='10.2.3.4',
prefixlen=16,
family=socket.AF_INET),
mock.call(controller.DELETE,
index=idx,
address='2001:db8::3',
prefixlen=64,
family=socket.AF_INET6)
])
mock_route.assert_has_calls([
mock.call(controller.DELETE,
oif=idx,
dst='10.0.0.0/8',
gateway='1.0.0.1',
table=10,
onlink=True,
family=socket.AF_INET),
mock.call(controller.DELETE,
oif=idx,
dst='20.0.0.0/8',
gateway='1.0.0.2',
prefsrc='1.2.3.4',
scope='link',
family=socket.AF_INET),
mock.call(controller.DELETE,
oif=idx,
dst='2001:db8:2::1/128',
gateway='2001:db8::1',
family=socket.AF_INET6)])
mock_rule.assert_has_calls([
mock.call(controller.DELETE,
src="1.1.1.1",
src_len=32,
table=20,
family=socket.AF_INET),
mock.call(controller.DELETE,
src="2001:db8::1",
src_len=128,
table=40,
family=socket.AF_INET6)])
mock_check_output.assert_has_calls([
mock.call(["post-down", "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('subprocess.check_output')
def test_down_with_errors(self, mock_check_output, mock_link_lookup,
mock_get_links, mock_link, mock_addr,
mock_route, mock_rule):
iface = interface_file.InterfaceFile(
name="eth1",
mtu=1450,
addresses=[{
consts.ADDRESS: '1.2.3.4',
consts.PREFIXLEN: 24
}, {
consts.ADDRESS: '10.2.3.4',
consts.PREFIXLEN: 16
}, {
consts.ADDRESS: '2001:db8::3',
consts.PREFIXLEN: 64
}],
routes=[{
consts.DST: '10.0.0.0/8',
consts.GATEWAY: '1.0.0.1',
consts.TABLE: 10,
consts.ONLINK: True
}, {
consts.DST: '20.0.0.0/8',
consts.GATEWAY: '1.0.0.2',
consts.PREFSRC: '1.2.3.4',
consts.SCOPE: 'link'
}, {
consts.DST: '2001:db8:2::1/128',
consts.GATEWAY: '2001:db8::1'
}],
rules=[{
consts.SRC: '1.1.1.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 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_UP
}]
mock_addr.side_effect = [
pyroute2.NetlinkError(123),
pyroute2.NetlinkError(123),
pyroute2.NetlinkError(123)
]
mock_route.side_effect = [
pyroute2.NetlinkError(123),
pyroute2.NetlinkError(123),
pyroute2.NetlinkError(123)
]
mock_rule.side_effect = [
pyroute2.NetlinkError(123),
pyroute2.NetlinkError(123),
]
mock_check_output.side_effect = [
Exception()
]
controller = interface.InterfaceController()
controller.down(iface)
mock_link.assert_called_once_with(
controller.SET,
index=idx,
state=consts.IFACE_DOWN)
mock_addr.assert_has_calls([
mock.call(controller.DELETE,
index=idx,
address='1.2.3.4',
prefixlen=24,
family=socket.AF_INET),
mock.call(controller.DELETE,
index=idx,
address='10.2.3.4',
prefixlen=16,
family=socket.AF_INET),
mock.call(controller.DELETE,
index=idx,
address='2001:db8::3',
prefixlen=64,
family=socket.AF_INET6)
])
mock_route.assert_has_calls([
mock.call(controller.DELETE,
oif=idx,
dst='10.0.0.0/8',
gateway='1.0.0.1',
table=10,
onlink=True,
family=socket.AF_INET),
mock.call(controller.DELETE,
oif=idx,
dst='20.0.0.0/8',
gateway='1.0.0.2',
prefsrc='1.2.3.4',
scope='link',
family=socket.AF_INET),
mock.call(controller.DELETE,
oif=idx,
dst='2001:db8:2::1/128',
gateway='2001:db8::1',
family=socket.AF_INET6)])
mock_rule.assert_has_calls([
mock.call(controller.DELETE,
src="1.1.1.1",
src_len=32,
table=20,
family=socket.AF_INET),
mock.call(controller.DELETE,
src="2001:db8::1",
src_len=128,
table=40,
family=socket.AF_INET6)])
mock_check_output.assert_has_calls([
mock.call(["post-down", "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('subprocess.check_output')
def test_down_already_down(self, mock_check_output, mock_link_lookup,
mock_get_links, mock_link, mock_addr,
mock_route, mock_rule):
iface = interface_file.InterfaceFile(
name="eth1",
mtu=1450,
addresses=[{
consts.ADDRESS: '1.2.3.4',
consts.PREFIXLEN: 24
}, {
consts.ADDRESS: '10.2.3.4',
consts.PREFIXLEN: 16
}, {
consts.ADDRESS: '2001:db8::3',
consts.PREFIXLEN: 64
}],
routes=[{
consts.DST: '10.0.0.0/8',
consts.GATEWAY: '1.0.0.1',
consts.TABLE: 10,
consts.ONLINK: True
}, {
consts.DST: '20.0.0.0/8',
consts.GATEWAY: '1.0.0.2',
consts.PREFSRC: '1.2.3.4',
consts.SCOPE: 'link'
}, {
consts.DST: '2001:db8:2::1/128',
consts.GATEWAY: '2001:db8::1'
}],
rules=[{
consts.SRC: '1.1.1.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 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
}]
controller = interface.InterfaceController()
controller.down(iface)
mock_link.assert_not_called()
mock_addr.assert_not_called()
mock_route.assert_not_called()
mock_rule.assert_not_called()
mock_check_output.assert_not_called()
@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_down_auto(self, mock_check_output, mock_link_lookup,
mock_get_links, mock_link, mock_addr, mock_route,
mock_rule):
iface = interface_file.InterfaceFile(
name="eth1",
mtu=1450,
addresses=[{
consts.DHCP: True,
consts.IPV6AUTO: True
}],
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_UP
}]
controller = interface.InterfaceController()
controller.down(iface)
mock_link.assert_called_once_with(
controller.SET,
index=idx,
state=consts.IFACE_DOWN)
mock_addr.assert_not_called()
mock_route.assert_not_called()
mock_rule.assert_not_called()
mock_check_output.assert_has_calls([
mock.call(["/sbin/dhclient",
"-r",
"-lf",
"/var/lib/dhclient/dhclient-{}.leases".format(
iface.name),
"-pf",
"/run/dhclient-{}.pid".format(iface.name),
iface.name], stderr=-2),
mock.call(["/sbin/sysctl", "-w",
"net.ipv6.conf.{}.accept_ra=0".format(iface.name)],
stderr=-2),
mock.call(["/sbin/sysctl", "-w",
"net.ipv6.conf.{}.autoconf=0".format(iface.name)],
stderr=-2),
mock.call(["post-down", iface.name])
])

View File

@ -0,0 +1,583 @@
# Copyright 2020 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 ipaddress
from unittest import mock
from octavia.amphorae.backends.utils import interface_file
from octavia.common import constants as consts
from octavia.tests.common import utils as test_utils
import octavia.tests.unit.base as base
class TestInterfaceFile(base.TestCase):
def test_vip_interface_file(self):
netns_interface = 'eth1234'
MTU = 1450
VIP_ADDRESS = '192.0.2.2'
SUBNET_CIDR = '192.0.2.0/24'
GATEWAY = '192.0.2.1'
DEST1 = '198.51.100.0/24'
NEXTHOP = '192.0.2.1'
VRRP_IP_ADDRESS = '192.10.2.4'
TOPOLOGY = 'SINGLE'
cidr = ipaddress.ip_network(SUBNET_CIDR)
prefixlen = cidr.prefixlen
vip_interface_file = interface_file.VIPInterfaceFile(
name=netns_interface,
mtu=MTU,
vip=VIP_ADDRESS,
ip_version=cidr.version,
prefixlen=prefixlen,
gateway=GATEWAY,
vrrp_ip=VRRP_IP_ADDRESS,
host_routes=[
{'destination': DEST1, 'nexthop': NEXTHOP}
],
topology=TOPOLOGY)
expected_dict = {
consts.NAME: netns_interface,
consts.MTU: MTU,
consts.ADDRESSES: [
{
consts.ADDRESS: VRRP_IP_ADDRESS,
consts.PREFIXLEN: prefixlen
},
{
consts.ADDRESS: VIP_ADDRESS,
consts.PREFIXLEN: prefixlen
}
],
consts.ROUTES: [
{
consts.DST: "0.0.0.0/0",
consts.GATEWAY: GATEWAY,
consts.FLAGS: [consts.ONLINK],
},
{
consts.DST: "0.0.0.0/0",
consts.GATEWAY: GATEWAY,
consts.FLAGS: [consts.ONLINK],
consts.TABLE: 1
},
{
consts.DST: cidr.exploded,
consts.PREFSRC: VIP_ADDRESS,
consts.SCOPE: 'link',
consts.TABLE: 1
},
{
consts.DST: DEST1,
consts.GATEWAY: NEXTHOP,
consts.FLAGS: [consts.ONLINK]
},
{
consts.DST: DEST1,
consts.GATEWAY: NEXTHOP,
consts.TABLE: 1,
consts.FLAGS: [consts.ONLINK]
}
],
consts.RULES: [
{
consts.SRC: VIP_ADDRESS,
consts.SRC_LEN: 32,
consts.TABLE: 1
}
],
consts.SCRIPTS: {
consts.IFACE_UP: [{
consts.COMMAND: (
"/usr/local/bin/lvs-masquerade.sh add ipv4 "
"{}".format(netns_interface))
}],
consts.IFACE_DOWN: [{
consts.COMMAND: (
"/usr/local/bin/lvs-masquerade.sh delete ipv4 "
"{}".format(netns_interface))
}]
}
}
with mock.patch('os.open'), mock.patch('os.fdopen'), mock.patch(
'octavia.amphorae.backends.utils.interface_file.'
'InterfaceFile.dump') as mock_dump:
vip_interface_file.write()
mock_dump.assert_called_once()
args = mock_dump.mock_calls[0][1]
test_utils.assert_interface_files_equal(
self, expected_dict, args[0])
def test_vip_interface_file_dhcp(self):
netns_interface = 'eth1234'
MTU = 1450
VIP_ADDRESS = '192.0.2.2'
SUBNET_CIDR = '192.0.2.0/24'
TOPOLOGY = 'SINGLE'
cidr = ipaddress.ip_network(SUBNET_CIDR)
prefixlen = cidr.prefixlen
vip_interface_file = interface_file.VIPInterfaceFile(
name=netns_interface,
mtu=MTU,
vip=VIP_ADDRESS,
ip_version=cidr.version,
prefixlen=prefixlen,
gateway=None,
vrrp_ip=None,
host_routes=[],
topology=TOPOLOGY)
expected_dict = {
consts.NAME: netns_interface,
consts.MTU: MTU,
consts.ADDRESSES: [
{
consts.DHCP: True
}, {
consts.ADDRESS: VIP_ADDRESS,
consts.PREFIXLEN: prefixlen
}
],
consts.ROUTES: [
{
consts.DST: cidr.exploded,
consts.PREFSRC: VIP_ADDRESS,
consts.SCOPE: 'link',
consts.TABLE: 1
}
],
consts.RULES: [
{
consts.SRC: VIP_ADDRESS,
consts.SRC_LEN: 32,
consts.TABLE: 1
}
],
consts.SCRIPTS: {
consts.IFACE_UP: [{
consts.COMMAND: (
"/usr/local/bin/lvs-masquerade.sh add ipv4 "
"{}".format(netns_interface))
}],
consts.IFACE_DOWN: [{
consts.COMMAND: (
"/usr/local/bin/lvs-masquerade.sh delete ipv4 "
"{}".format(netns_interface))
}]
}
}
with mock.patch('os.open'), mock.patch('os.fdopen'), mock.patch(
'octavia.amphorae.backends.utils.interface_file.'
'InterfaceFile.dump') as mock_dump:
vip_interface_file.write()
mock_dump.assert_called_once()
args = mock_dump.mock_calls[0][1]
test_utils.assert_interface_files_equal(
self, expected_dict, args[0])
def test_vip_interface_file_active_standby(self):
netns_interface = 'eth1234'
MTU = 1450
VIP_ADDRESS = '192.0.2.2'
SUBNET_CIDR = '192.0.2.0/24'
GATEWAY = '192.0.2.1'
VRRP_IP_ADDRESS = '192.10.2.4'
TOPOLOGY = 'ACTIVE_STANDBY'
cidr = ipaddress.ip_network(SUBNET_CIDR)
prefixlen = cidr.prefixlen
vip_interface_file = interface_file.VIPInterfaceFile(
name=netns_interface,
mtu=MTU,
vip=VIP_ADDRESS,
ip_version=cidr.version,
prefixlen=prefixlen,
gateway=GATEWAY,
vrrp_ip=VRRP_IP_ADDRESS,
host_routes=[],
topology=TOPOLOGY)
expected_dict = {
consts.NAME: netns_interface,
consts.MTU: MTU,
consts.ADDRESSES: [
{
consts.ADDRESS: VRRP_IP_ADDRESS,
consts.PREFIXLEN: prefixlen
}
],
consts.ROUTES: [
{
consts.DST: "0.0.0.0/0",
consts.GATEWAY: GATEWAY,
consts.FLAGS: [consts.ONLINK],
},
{
consts.DST: "0.0.0.0/0",
consts.GATEWAY: GATEWAY,
consts.FLAGS: [consts.ONLINK],
consts.TABLE: 1
}
],
consts.RULES: [],
consts.SCRIPTS: {
consts.IFACE_UP: [{
consts.COMMAND: (
"/usr/local/bin/lvs-masquerade.sh add ipv4 "
"{}".format(netns_interface))
}],
consts.IFACE_DOWN: [{
consts.COMMAND: (
"/usr/local/bin/lvs-masquerade.sh delete ipv4 "
"{}".format(netns_interface))
}]
}
}
with mock.patch('os.open'), mock.patch('os.fdopen'), mock.patch(
'octavia.amphorae.backends.utils.interface_file.'
'InterfaceFile.dump') as mock_dump:
vip_interface_file.write()
mock_dump.assert_called_once()
args = mock_dump.mock_calls[0][1]
test_utils.assert_interface_files_equal(
self, expected_dict, args[0])
def test_vip_interface_file_ipv6(self):
netns_interface = 'eth1234'
MTU = 1450
VIP_ADDRESS = '2001:db8::7'
SUBNET_CIDR = '2001:db8::/64'
GATEWAY = '2001:db8::1'
DEST1 = '2001:db8:2::/64'
NEXTHOP = '2001:db8:2::1'
VRRP_IP_ADDRESS = '2001:db8::42'
TOPOLOGY = 'SINGLE'
cidr = ipaddress.ip_network(SUBNET_CIDR)
prefixlen = cidr.prefixlen
vip_interface_file = interface_file.VIPInterfaceFile(
name=netns_interface,
mtu=MTU,
vip=VIP_ADDRESS,
ip_version=cidr.version,
prefixlen=prefixlen,
gateway=GATEWAY,
vrrp_ip=VRRP_IP_ADDRESS,
host_routes=[
{'destination': DEST1, 'nexthop': NEXTHOP}
],
topology=TOPOLOGY)
expected_dict = {
consts.NAME: netns_interface,
consts.MTU: MTU,
consts.ADDRESSES: [
{
consts.ADDRESS: VRRP_IP_ADDRESS,
consts.PREFIXLEN: prefixlen
},
{
consts.ADDRESS: VIP_ADDRESS,
consts.PREFIXLEN: prefixlen
}
],
consts.ROUTES: [
{
consts.DST: "::/0",
consts.GATEWAY: GATEWAY,
consts.FLAGS: [consts.ONLINK]
},
{
consts.DST: "::/0",
consts.GATEWAY: GATEWAY,
consts.TABLE: 1,
consts.FLAGS: [consts.ONLINK]
},
{
consts.DST: cidr.exploded,
consts.PREFSRC: VIP_ADDRESS,
consts.SCOPE: 'link',
consts.TABLE: 1
},
{
consts.DST: DEST1,
consts.GATEWAY: NEXTHOP,
consts.FLAGS: [consts.ONLINK]
},
{
consts.DST: DEST1,
consts.GATEWAY: NEXTHOP,
consts.TABLE: 1,
consts.FLAGS: [consts.ONLINK]
}
],
consts.RULES: [
{
consts.SRC: VIP_ADDRESS,
consts.SRC_LEN: 128,
consts.TABLE: 1
}
],
consts.SCRIPTS: {
consts.IFACE_UP: [{
consts.COMMAND: (
"/usr/local/bin/lvs-masquerade.sh add ipv6 "
"{}".format(netns_interface))
}],
consts.IFACE_DOWN: [{
consts.COMMAND: (
"/usr/local/bin/lvs-masquerade.sh delete ipv6 "
"{}".format(netns_interface))
}]
}
}
with mock.patch('os.open'), mock.patch('os.fdopen'), mock.patch(
'octavia.amphorae.backends.utils.interface_file.'
'InterfaceFile.dump') as mock_dump:
vip_interface_file.write()
mock_dump.assert_called_once()
args = mock_dump.mock_calls[0][1]
test_utils.assert_interface_files_equal(
self, expected_dict, args[0])
def test_port_interface_file(self):
netns_interface = 'eth1234'
FIXED_IP = '192.0.2.2'
SUBNET_CIDR = '192.0.2.0/24'
DEST1 = '198.51.100.0/24'
DEST2 = '203.0.113.0/24'
DEST3 = 'fd01::/64'
NEXTHOP = '192.0.2.1'
NEXTHOP2 = '2001:db7::8'
MTU = 1450
FIXED_IP_IPV6 = '2001:0db8:0000:0000:0000:0000:0000:0001'
SUBNET_CIDR_IPV6 = '2001:db8::/64'
fixed_ips = [{'ip_address': FIXED_IP,
'subnet_cidr': SUBNET_CIDR,
'host_routes': [
{'destination': DEST1, 'nexthop': NEXTHOP},
{'destination': DEST2, 'nexthop': NEXTHOP}
]},
{'ip_address': FIXED_IP_IPV6,
'subnet_cidr': SUBNET_CIDR_IPV6,
'host_routes': [
{'destination': DEST3, 'nexthop': NEXTHOP2}
]},
]
port_interface_file = interface_file.PortInterfaceFile(
name=netns_interface,
fixed_ips=fixed_ips,
mtu=MTU)
expected_dict = {
consts.NAME: netns_interface,
consts.MTU: MTU,
consts.ADDRESSES: [
{
consts.ADDRESS: FIXED_IP,
consts.PREFIXLEN: (
ipaddress.ip_network(SUBNET_CIDR).prefixlen
)
},
{
consts.ADDRESS: FIXED_IP_IPV6,
consts.PREFIXLEN: (
ipaddress.ip_network(SUBNET_CIDR_IPV6).prefixlen
)
}
],
consts.ROUTES: [
{
consts.DST: DEST1,
consts.GATEWAY: NEXTHOP
},
{
consts.DST: DEST2,
consts.GATEWAY: NEXTHOP
},
{
consts.DST: DEST3,
consts.GATEWAY: NEXTHOP2
}
],
consts.RULES: [],
consts.SCRIPTS: {
consts.IFACE_UP: [{
consts.COMMAND: (
"/usr/local/bin/lvs-masquerade.sh add ipv4 "
"{}".format(netns_interface))
}, {
consts.COMMAND: (
"/usr/local/bin/lvs-masquerade.sh add ipv6 "
"{}".format(netns_interface))
}],
consts.IFACE_DOWN: [{
consts.COMMAND: (
"/usr/local/bin/lvs-masquerade.sh delete ipv4 "
"{}".format(netns_interface))
}, {
consts.COMMAND: (
"/usr/local/bin/lvs-masquerade.sh delete ipv6 "
"{}".format(netns_interface))
}]
}
}
with mock.patch('os.open'), mock.patch('os.fdopen'), mock.patch(
'octavia.amphorae.backends.utils.interface_file.'
'InterfaceFile.dump') as mock_dump:
port_interface_file.write()
mock_dump.assert_called_once()
args = mock_dump.mock_calls[0][1]
test_utils.assert_interface_files_equal(
self, expected_dict, args[0])
def test_port_interface_file_dhcp(self):
netns_interface = 'eth1234'
MTU = 1450
port_interface_file = interface_file.PortInterfaceFile(
name=netns_interface,
fixed_ips=None,
mtu=MTU)
expected_dict = {
consts.NAME: netns_interface,
consts.MTU: MTU,
consts.ADDRESSES: [{
consts.DHCP: True,
consts.IPV6AUTO: True,
}],
consts.ROUTES: [],
consts.RULES: [],
consts.SCRIPTS: {
consts.IFACE_UP: [{
consts.COMMAND: (
"/usr/local/bin/lvs-masquerade.sh add ipv4 "
"{}".format(netns_interface))
}, {
consts.COMMAND: (
"/usr/local/bin/lvs-masquerade.sh add ipv6 "
"{}".format(netns_interface))
}],
consts.IFACE_DOWN: [{
consts.COMMAND: (
"/usr/local/bin/lvs-masquerade.sh delete ipv4 "
"{}".format(netns_interface))
}, {
consts.COMMAND: (
"/usr/local/bin/lvs-masquerade.sh delete ipv6 "
"{}".format(netns_interface))
}]
}
}
with mock.patch('os.open'), mock.patch('os.fdopen'), mock.patch(
'octavia.amphorae.backends.utils.interface_file.'
'InterfaceFile.dump') as mock_dump:
port_interface_file.write()
mock_dump.assert_called_once()
args = mock_dump.mock_calls[0][1]
test_utils.assert_interface_files_equal(
self, expected_dict, args[0])
def test_from_file(self):
filename = 'interface.json'
content = ('{"addresses": [\n'
'{"address": "10.0.0.181",\n'
'"prefixlen": 26}\n'
'],\n'
'"mtu": 1450,\n'
'"name": "eth1",\n'
'"routes": [\n'
'{"dst": "0.0.0.0/0",\n'
'"gateway": "10.0.0.129",\n'
'"onlink": true}\n'
'],\n'
'"rules": [\n'
'{"src": "10.0.0.157",\n'
'"src_len": 32,\n'
'"table": 1}\n'
'],\n'
'"scripts": {\n'
'"down": [\n'
'{"command": "script-down"}\n'
'], "up": [ \n'
'{"command": "script-up"}\n'
']}}\n')
self.useFixture(
test_utils.OpenFixture(filename,
contents=content))
iface = interface_file.InterfaceFile.from_file(filename)
expected_dict = {
consts.NAME: "eth1",
consts.MTU: 1450,
consts.ADDRESSES: [{
consts.ADDRESS: "10.0.0.181",
consts.PREFIXLEN: 26
}],
consts.ROUTES: [{
consts.DST: "0.0.0.0/0",
consts.GATEWAY: "10.0.0.129",
consts.FLAGS: [consts.ONLINK]
}],
consts.RULES: [{
consts.SRC: "10.0.0.157",
consts.SRC_LEN: 32,
consts.TABLE: 1
}],
consts.SCRIPTS: {
consts.IFACE_UP: [{
consts.COMMAND: "script-up"
}],
consts.IFACE_DOWN: [{
consts.COMMAND: "script-down"
}]
}
}
self.assertEqual(expected_dict[consts.NAME], iface.name)
self.assertEqual(expected_dict[consts.MTU], iface.mtu)
test_utils.assert_address_lists_equal(
self, expected_dict[consts.ADDRESSES], iface.addresses)
test_utils.assert_rule_lists_equal(
self, expected_dict[consts.RULES], iface.rules)
test_utils.assert_script_lists_equal(
self, expected_dict[consts.SCRIPTS], iface.scripts)

View File

@ -0,0 +1,137 @@
# Copyright 2021 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.
from unittest import mock
from octavia.amphorae.backends.utils import interface_file
from octavia.cmd import interface
from octavia.tests.unit import base
class TestInterfaceCMD(base.TestCase):
def setUp(self):
super().setUp()
self.interface1 = interface_file.InterfaceFile("eth1")
self.interface2 = interface_file.InterfaceFile("eth2")
def test_interfaces_find(self):
controller = mock.Mock()
controller.list = mock.Mock()
controller.list.return_value = {
"eth1": self.interface1,
"eth2": self.interface2
}
ret = interface.interfaces_find(controller, "eth2")
self.assertCountEqual([self.interface2], ret)
controller.list.assert_called_once()
def test_interfaces_find_all(self):
controller = mock.Mock()
controller.list = mock.Mock()
controller.list.return_value = {
"eth1": self.interface1,
"eth2": self.interface2
}
ret = interface.interfaces_find(controller, "all")
self.assertCountEqual([self.interface1, self.interface2], ret)
controller.list.assert_called_once()
def test_interfaces_find_all_empty(self):
controller = mock.Mock()
controller.list = mock.Mock()
controller.list.return_value = {}
ret = interface.interfaces_find(controller, "all")
self.assertEqual(0, len(ret))
controller.list.assert_called_once()
def test_interfaces_find_not_found(self):
controller = mock.Mock()
controller.list = mock.Mock()
controller.list.return_value = {
"eth1": self.interface1,
"eth2": self.interface2
}
self.assertRaisesRegex(
interface.InterfaceException,
"Could not find interface 'eth3'.",
interface.interfaces_find,
controller, "eth3")
controller.list.assert_called_once()
def test_interfaces_update(self):
action_fn = mock.Mock()
action_str = mock.Mock()
interfaces = [self.interface1, self.interface2]
interface.interfaces_update(interfaces, action_fn, action_str)
self.assertEqual(2, len(action_fn.mock_calls))
action_fn.assert_called_with(self.interface2)
def test_interfaces_update_with_errors(self):
action_fn = mock.Mock()
action_str = mock.Mock()
interfaces = [self.interface1, self.interface2]
action_fn.side_effect = [None, Exception("error msg")]
self.assertRaisesRegex(
interface.InterfaceException,
"Could not configure interface:.*eth2.*error msg",
interface.interfaces_update,
interfaces, action_fn, action_str)
self.assertEqual(2, len(action_fn.mock_calls))
@mock.patch("octavia.amphorae.backends.utils.interface."
"InterfaceController")
@mock.patch("octavia.cmd.interface.interfaces_find")
@mock.patch("octavia.cmd.interface.interfaces_update")
def test_interface_cmd(self, mock_interfaces_update,
mock_interfaces_find, mock_controller):
controller = mock.Mock()
controller.up = mock.Mock()
controller.down = mock.Mock()
mock_controller.return_value = controller
mock_interfaces_find.return_value = [self.interface1]
interface.interface_cmd("eth1", "up")
mock_interfaces_find.assert_called_once_with(
controller, "eth1")
mock_interfaces_update.assert_called_once_with(
[self.interface1], mock_controller.return_value.up, "up")
mock_interfaces_find.reset_mock()
mock_interfaces_update.reset_mock()
mock_interfaces_find.return_value = [self.interface2]
interface.interface_cmd("eth2", "down")
mock_interfaces_find.assert_called_once_with(
controller, "eth2")
mock_interfaces_update.assert_called_once_with(
[self.interface2], mock_controller.return_value.down, "down")
@mock.patch("octavia.amphorae.backends.utils.interface."
"InterfaceController")
def test_interface_cmd_invalid_action(self, mock_controller):
self.assertRaisesRegex(
interface.InterfaceException,
"Unknown action.*invalidaction",
interface.interface_cmd,
"eth1", "invalidaction")

View File

@ -0,0 +1,13 @@
---
deprecations:
- |
The ``[amphora_agent].agent_server_network_file`` configuration option is
now deprecated, the new Amphora network configuration tool introduced in
Xena does not support a single configuration file.
fixes:
- |
Amphora network configuration for the VIP interface and the pool member
interfaces are now applied with the amphora-interface tool.
amphora-interface uses pyroute2 low-level functions to configure the
interfaces instead of distribution-specific tools such as "network-scripts"
or "/etc/network/interfaces" files.

View File

@ -53,6 +53,7 @@ console_scripts =
haproxy-vrrp-check = octavia.cmd.haproxy_vrrp_check:main
octavia-status = octavia.cmd.status:main
amphora-health-checker = octavia.cmd.health_checker:main
amphora-interface = octavia.cmd.interface:main
octavia.api.drivers =
noop_driver = octavia.api.drivers.noop_driver.driver:NoopProviderDriver
noop_driver-alt = octavia.api.drivers.noop_driver.driver:NoopProviderDriver