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:
parent
39735ebf10
commit
5dd7ad9ad8
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
)
|
||||
|
@ -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):
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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 +
|
||||
|
@ -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
|
||||
|
@ -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 %}
|
||||
|
@ -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 }}
|
@ -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 %}
|
||||
|
@ -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
|
@ -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
|
@ -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 %}
|
||||
|
@ -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 }}"
|
||||
|
@ -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 %}
|
@ -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
|
@ -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" \
|
||||
|
@ -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
|
||||
|
||||
|
@ -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)
|
||||
|
||||
|
||||
|
@ -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 }}
|
||||
|
237
octavia/amphorae/backends/utils/interface.py
Normal file
237
octavia/amphorae/backends/utils/interface.py
Normal 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)
|
218
octavia/amphorae/backends/utils/interface_file.py
Normal file
218
octavia/amphorae/backends/utils/interface_file.py
Normal 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
90
octavia/cmd/interface.py
Normal 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()
|
@ -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.")),
|
||||
|
@ -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/'
|
||||
|
@ -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')
|
||||
|
@ -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])
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -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()
|
||||
|
@ -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()
|
||||
|
@ -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')
|
||||
|
||||
|
@ -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",
|
||||
|
902
octavia/tests/unit/amphorae/backends/utils/test_interface.py
Normal file
902
octavia/tests/unit/amphorae/backends/utils/test_interface.py
Normal 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])
|
||||
])
|
@ -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)
|
137
octavia/tests/unit/cmd/test_interface.py
Normal file
137
octavia/tests/unit/cmd/test_interface.py
Normal 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")
|
@ -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.
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user