Run amphora haproxy in a network namespace
In the current Octavia there is the possibility of an address space conflict between the Octavia load balancer management network and a tenant network. This patch puts the haproxy processes inside the amphora into a network namespace to provide isolation from the load balancer management network. A new file /var/lib/octavia/plugged_interfaces is created and interfaces are writted to it on every plugVIP or plugNetwork call. Interfaces in this file are created under the network namespace. Change-Id: I75472885fe45226a5315867369eaef9b001a112b Co-Authored-By: Bharath M <bharath.stacker@gmail.com> Closes-Bug: #1458920
This commit is contained in:
parent
5d5dcf9951
commit
b89abe1871
|
@ -1,6 +1,7 @@
|
|||
description "Start up the Octavia Amphora Agent"
|
||||
|
||||
start on startup
|
||||
start on runlevel [2345]
|
||||
stop on runlevel [!2345]
|
||||
|
||||
respawn
|
||||
respawn limit 2 2
|
||||
|
|
|
@ -202,7 +202,7 @@
|
|||
[amphora_agent]
|
||||
# agent_server_ca = /etc/octavia/certs/client_ca.pem
|
||||
# agent_server_cert = /etc/octavia/certs/server.pem
|
||||
# agent_server_network_dir = /etc/network/interfaces.d/
|
||||
# agent_server_network_dir = /etc/netns/amphora-haproxy/network/interfaces.d/
|
||||
# agent_server_network_file =
|
||||
|
||||
[keepalived_vrrp]
|
||||
|
@ -271,4 +271,4 @@
|
|||
|
||||
# CA certificates file to verify neutron connections when TLS is enabled
|
||||
# insecure = False
|
||||
# ca_certificates_file =
|
||||
# ca_certificates_file =
|
||||
|
|
|
@ -21,6 +21,7 @@ import subprocess
|
|||
import flask
|
||||
import ipaddress
|
||||
import netifaces
|
||||
import pyroute2
|
||||
import six
|
||||
|
||||
from octavia.amphorae.backends.agent import api_server
|
||||
|
@ -80,13 +81,6 @@ def _get_version_of_installed_package(name):
|
|||
return m.group(0)[len('Version: '):]
|
||||
|
||||
|
||||
def _get_network_bytes(interface, type):
|
||||
file_name = "/sys/class/net/{interface}/statistics/{type}_bytes".format(
|
||||
interface=interface, type=type)
|
||||
with open(file_name, 'r') as f:
|
||||
return f.readline()
|
||||
|
||||
|
||||
def _count_haproxy_processes(listener_list):
|
||||
num = 0
|
||||
for listener_id in listener_list:
|
||||
|
@ -133,20 +127,32 @@ def _load():
|
|||
|
||||
def _get_networks():
|
||||
networks = dict()
|
||||
for interface in netifaces.interfaces():
|
||||
if not interface.startswith('eth') or ":" in interface:
|
||||
continue
|
||||
networks[interface] = dict(
|
||||
network_tx=_get_network_bytes(interface, 'tx'),
|
||||
network_rx=_get_network_bytes(interface, 'rx'))
|
||||
with pyroute2.NetNS(consts.AMPHORA_NAMESPACE) as netns:
|
||||
for interface in netns.get_links():
|
||||
interface_name = None
|
||||
for item in interface['attrs']:
|
||||
if item[0] == 'IFLA_IFNAME' and not item[1].startswith('eth'):
|
||||
break
|
||||
elif item[0] == 'IFLA_IFNAME':
|
||||
interface_name = item[1]
|
||||
if item[0] == 'IFLA_STATS64':
|
||||
networks[interface_name] = dict(
|
||||
network_tx=item[1]['tx_bytes'],
|
||||
network_rx=item[1]['rx_bytes'])
|
||||
return networks
|
||||
|
||||
|
||||
def get_interface(ip_addr):
|
||||
if six.PY2:
|
||||
ip_version = ipaddress.ip_address(unicode(ip_addr)).version
|
||||
else:
|
||||
ip_version = ipaddress.ip_address(ip_addr).version
|
||||
|
||||
try:
|
||||
if six.PY2:
|
||||
ip_version = ipaddress.ip_address(unicode(ip_addr)).version
|
||||
else:
|
||||
ip_version = ipaddress.ip_address(ip_addr).version
|
||||
except Exception:
|
||||
return flask.make_response(
|
||||
flask.jsonify(dict(message="Invalid IP address")), 400)
|
||||
|
||||
if ip_version == 4:
|
||||
address_format = netifaces.AF_INET
|
||||
elif ip_version == 6:
|
||||
|
@ -154,12 +160,50 @@ def get_interface(ip_addr):
|
|||
else:
|
||||
return flask.make_response(
|
||||
flask.jsonify(dict(message="Bad IP address version")), 400)
|
||||
for interface in netifaces.interfaces():
|
||||
for i in netifaces.ifaddresses(interface)[address_format]:
|
||||
if i['addr'] == ip_addr:
|
||||
return flask.make_response(
|
||||
flask.jsonify(dict(message='OK', interface=interface)),
|
||||
200)
|
||||
|
||||
# We need to normalize the address as IPv6 has multiple representations
|
||||
# fe80:0000:0000:0000:f816:3eff:fef2:2058 == fe80::f816:3eff:fef2:2058
|
||||
normalized_addr = socket.inet_ntop(address_format,
|
||||
socket.inet_pton(address_format,
|
||||
ip_addr))
|
||||
|
||||
with pyroute2.NetNS(consts.AMPHORA_NAMESPACE) as netns:
|
||||
for addr in netns.get_addr():
|
||||
# Save the interface index as IPv6 records don't list a
|
||||
# textual interface
|
||||
interface_idx = addr['index']
|
||||
# Save the address family (IPv4/IPv6) for use normalizing
|
||||
# the IP address for comparison
|
||||
interface_af = addr['family']
|
||||
# Search through the attributes of each address record
|
||||
for attr in addr['attrs']:
|
||||
# Look for the attribute name/value pair for the address
|
||||
if attr[0] == 'IFA_ADDRESS':
|
||||
# Compare the normalized address with the address we
|
||||
# we are looking for. Since we have matched the name
|
||||
# above, attr[1] is the address value
|
||||
if normalized_addr == socket.inet_ntop(
|
||||
interface_af,
|
||||
socket.inet_pton(interface_af, attr[1])):
|
||||
|
||||
# Lookup the matching interface name by
|
||||
# getting the interface with the index we found
|
||||
# in the above address search
|
||||
lookup_int = netns.get_links(interface_idx)
|
||||
# Search through the attributes of the matching
|
||||
# interface record
|
||||
for int_attr in lookup_int[0]['attrs']:
|
||||
# Look for the attribute name/value pair
|
||||
# that includes the interface name
|
||||
if int_attr[0] == 'IFLA_IFNAME':
|
||||
# Return the response with the matching
|
||||
# interface name that is in int_attr[1]
|
||||
# for the matching interface attribute
|
||||
# name
|
||||
return flask.make_response(
|
||||
flask.jsonify(
|
||||
dict(message='OK',
|
||||
interface=int_attr[1])), 200)
|
||||
|
||||
return flask.make_response(
|
||||
flask.jsonify(dict(message="Error interface not found "
|
||||
|
|
|
@ -62,7 +62,8 @@ def upload_keepalived_config():
|
|||
keepalived_pid=util.keepalived_pid_path(),
|
||||
keepalived_cmd=consts.KEEPALIVED_CMD,
|
||||
keepalived_cfg=util.keepalived_cfg_path(),
|
||||
keepalived_log=util.keepalived_log_path()
|
||||
keepalived_log=util.keepalived_log_path(),
|
||||
amphora_nsname=consts.AMPHORA_NAMESPACE
|
||||
)
|
||||
text_file.write(text)
|
||||
|
||||
|
|
|
@ -138,7 +138,8 @@ def upload_haproxy_config(amphora_id, listener_id):
|
|||
haproxy_cmd=util.CONF.haproxy_amphora.haproxy_cmd,
|
||||
haproxy_cfg=util.config_path(listener_id),
|
||||
respawn_count=util.CONF.haproxy_amphora.respawn_count,
|
||||
respawn_interval=util.CONF.haproxy_amphora.respawn_interval
|
||||
respawn_interval=util.CONF.haproxy_amphora.respawn_interval,
|
||||
amphora_nsname=consts.AMPHORA_NAMESPACE
|
||||
)
|
||||
text_file.write(text)
|
||||
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
import socket
|
||||
import stat
|
||||
import subprocess
|
||||
|
@ -26,6 +27,8 @@ from werkzeug import exceptions
|
|||
|
||||
from octavia.amphorae.backends.agent.api_server import util
|
||||
from octavia.common import constants as consts
|
||||
from octavia.i18n import _LE, _LI
|
||||
|
||||
|
||||
ETH_PORT_CONF = 'plug_vip_ethX.conf.j2'
|
||||
|
||||
|
@ -48,18 +51,39 @@ def plug_vip(vip, subnet_cidr, gateway, mac_address):
|
|||
message="Invalid VIP")), 400)
|
||||
|
||||
interface = _interface_by_mac(mac_address)
|
||||
primary_interface = "{interface}".format(interface=interface)
|
||||
secondary_interface = "{interface}:0".format(interface=interface)
|
||||
|
||||
# assume for now only a fixed subnet size
|
||||
sections = vip.split('.')[:3]
|
||||
sections.append('255')
|
||||
broadcast = '.'.join(sections)
|
||||
|
||||
# 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.
|
||||
interface_file_path = util.get_network_interface_file(interface)
|
||||
os.makedirs('/etc/netns/' + consts.AMPHORA_NAMESPACE)
|
||||
shutil.copytree('/etc/network',
|
||||
'/etc/netns/{}/network'.format(consts.AMPHORA_NAMESPACE),
|
||||
symlinks=True,
|
||||
ignore=shutil.ignore_patterns('eth0*', 'openssh*'))
|
||||
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 file:
|
||||
file.write('auto lo\n')
|
||||
file.write('iface lo inet loopback\n')
|
||||
file.write('source /etc/netns/{}/network/interfaces.d/*.cfg\n'.format(
|
||||
consts.AMPHORA_NAMESPACE))
|
||||
|
||||
# write interface file
|
||||
mode = stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH
|
||||
name = util.get_network_interface_file(interface)
|
||||
flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC
|
||||
|
||||
with os.fdopen(os.open(name, flags, mode), 'w') as text_file:
|
||||
with os.fdopen(os.open(interface_file_path, flags, mode),
|
||||
'w') as text_file:
|
||||
text = template_vip.render(
|
||||
interface=interface,
|
||||
vip=vip,
|
||||
|
@ -68,46 +92,24 @@ def plug_vip(vip, subnet_cidr, gateway, mac_address):
|
|||
netmask='255.255.255.0')
|
||||
text_file.write(text)
|
||||
|
||||
# Update the list of interfaces to add to the namespace
|
||||
# This is used in the amphora reboot case to re-establish the namespace
|
||||
_update_plugged_interfaces_file(interface, mac_address)
|
||||
|
||||
# Create the namespace
|
||||
netns = pyroute2.NetNS(consts.AMPHORA_NAMESPACE, flags=os.O_CREAT)
|
||||
netns.close()
|
||||
|
||||
with pyroute2.IPRoute() as ipr:
|
||||
# Move the interfaces into the namespace
|
||||
idx = ipr.link_lookup(ifname=primary_interface)[0]
|
||||
ipr.link('set', index=idx, net_ns_fd=consts.AMPHORA_NAMESPACE)
|
||||
|
||||
# bring interfaces up
|
||||
_bring_if_down("{interface}".format(interface=interface))
|
||||
_bring_if_down("{interface}:0".format(interface=interface))
|
||||
_bring_if_up("{interface}".format(interface=interface), 'VIP')
|
||||
_bring_if_up("{interface}:0".format(interface=interface), 'VIP')
|
||||
|
||||
# Setup policy based routes for the amphora
|
||||
|
||||
ip = pyroute2.IPRoute()
|
||||
|
||||
cidr_split = subnet_cidr.split('/')
|
||||
|
||||
num_interface = ip.link_lookup(ifname=interface)
|
||||
|
||||
ip.route('add',
|
||||
dst=cidr_split[0],
|
||||
mask=int(cidr_split[1]),
|
||||
oif=num_interface,
|
||||
table=1,
|
||||
rtproto='RTPROT_BOOT',
|
||||
rtscope='RT_SCOPE_LINK')
|
||||
|
||||
ip.route('add',
|
||||
dst='0.0.0.0',
|
||||
gateway=gateway,
|
||||
oif=num_interface,
|
||||
table=1,
|
||||
rtproto='RTPROT_BOOT')
|
||||
|
||||
ip.rule('add',
|
||||
table=1,
|
||||
action='FR_ACT_TO_TBL',
|
||||
src=cidr_split[0],
|
||||
src_len=int(cidr_split[1]))
|
||||
|
||||
ip.rule('add',
|
||||
table=1,
|
||||
action='FR_ACT_TO_TBL',
|
||||
dst=cidr_split[0],
|
||||
dst_len=int(cidr_split[1]))
|
||||
_bring_if_down(primary_interface)
|
||||
_bring_if_down(secondary_interface)
|
||||
_bring_if_up(primary_interface, 'VIP')
|
||||
_bring_if_up(secondary_interface, 'VIP')
|
||||
|
||||
return flask.make_response(flask.jsonify(dict(
|
||||
message="OK",
|
||||
|
@ -116,25 +118,52 @@ def plug_vip(vip, subnet_cidr, gateway, mac_address):
|
|||
|
||||
|
||||
def plug_network(mac_address):
|
||||
interface = _interface_by_mac(mac_address)
|
||||
# This is the interface as it was initially plugged into the
|
||||
# default network namespace, this will likely always be eth1
|
||||
default_netns_interface = _interface_by_mac(mac_address)
|
||||
|
||||
# We need to determine the interface name when inside the namespace
|
||||
# to avoid name conflicts
|
||||
with pyroute2.NetNS(consts.AMPHORA_NAMESPACE, flags=os.O_CREAT) as netns:
|
||||
|
||||
# 1 means just loopback, but we should already have a VIP
|
||||
# This works for the add/delete/add case as we don't delete interfaces
|
||||
# Note, eth0 is skipped because that is the VIP interface
|
||||
netns_interface = 'eth{0}'.format(len(netns.get_links()))
|
||||
|
||||
LOG.info(_LI('Plugged interface {0} will become {1} in the '
|
||||
'namespace {2}').format(default_netns_interface,
|
||||
netns_interface,
|
||||
consts.AMPHORA_NAMESPACE))
|
||||
interface_file_path = util.get_network_interface_file(netns_interface)
|
||||
|
||||
# write interface file
|
||||
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
|
||||
name = util.get_network_interface_file(interface)
|
||||
|
||||
with os.fdopen(os.open(name, flags, mode), 'w') as text_file:
|
||||
text = template_port.render(interface=interface)
|
||||
with os.fdopen(os.open(interface_file_path, flags, mode),
|
||||
'w') as text_file:
|
||||
text = template_port.render(interface=netns_interface)
|
||||
text_file.write(text)
|
||||
|
||||
_bring_if_down(interface)
|
||||
_bring_if_up(interface, 'network')
|
||||
# Update the list of interfaces to add to the namespace
|
||||
_update_plugged_interfaces_file(netns_interface, mac_address)
|
||||
|
||||
with pyroute2.IPRoute() as ipr:
|
||||
# Move the interfaces into the namespace
|
||||
idx = ipr.link_lookup(ifname=default_netns_interface)[0]
|
||||
ipr.link('set', index=idx,
|
||||
net_ns_fd=consts.AMPHORA_NAMESPACE,
|
||||
IFLA_IFNAME=netns_interface)
|
||||
|
||||
_bring_if_down(netns_interface)
|
||||
_bring_if_up(netns_interface, 'network')
|
||||
|
||||
return flask.make_response(flask.jsonify(dict(
|
||||
message="OK",
|
||||
details="Plugged on interface {interface}".format(
|
||||
interface=interface))), 202)
|
||||
interface=netns_interface))), 202)
|
||||
|
||||
|
||||
def _interface_by_mac(mac):
|
||||
|
@ -148,22 +177,41 @@ def _interface_by_mac(mac):
|
|||
details="No suitable network interface found")), 404))
|
||||
|
||||
|
||||
def _bring_if_up(params, what):
|
||||
# bring interface up
|
||||
cmd = "ifup {params}".format(params=params)
|
||||
def _bring_if_up(interface, what):
|
||||
# Note, we are not using pyroute2 for this as it is not /etc/netns
|
||||
# aware.
|
||||
cmd = ("ip netns exec {ns} ifup {params}".format(
|
||||
ns=consts.AMPHORA_NAMESPACE, params=interface))
|
||||
try:
|
||||
subprocess.check_output(cmd.split(), stderr=subprocess.STDOUT)
|
||||
except subprocess.CalledProcessError as e:
|
||||
LOG.debug("Failed to if up %s", e)
|
||||
LOG.error(_LE('Failed to if up {0} due to '
|
||||
'error: {1}').format(interface, str(e)))
|
||||
raise exceptions.HTTPException(
|
||||
response=flask.make_response(flask.jsonify(dict(
|
||||
message='Error plugging {0}'.format(what),
|
||||
details=e.output)), 500))
|
||||
|
||||
|
||||
def _bring_if_down(params):
|
||||
cmd = "ifdown {params}".format(params=params)
|
||||
def _bring_if_down(interface):
|
||||
# Note, we are not using pyroute2 for this as it is not /etc/netns
|
||||
# aware.
|
||||
cmd = ("ip netns exec {ns} ifdown {params}".format(
|
||||
ns=consts.AMPHORA_NAMESPACE, params=interface))
|
||||
try:
|
||||
subprocess.check_output(cmd.split(), stderr=subprocess.STDOUT)
|
||||
except subprocess.CalledProcessError:
|
||||
pass
|
||||
|
||||
|
||||
def _update_plugged_interfaces_file(interface, mac_address):
|
||||
# write interfaces to plugged_interfaces file and prevent duplicates
|
||||
plug_inf_file = consts.PLUGGED_INTERFACES
|
||||
flags = os.O_RDWR | os.O_CREAT
|
||||
# mode 0644
|
||||
mode = stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH
|
||||
with os.fdopen(os.open(plug_inf_file, flags, mode), 'r+') as text_file:
|
||||
inf_list = [inf.split()[0].rstrip() for inf in text_file]
|
||||
if mac_address not in inf_list:
|
||||
text_file.write("{mac_address} {interface}\n".format(
|
||||
mac_address=mac_address, interface=interface))
|
||||
|
|
|
@ -22,7 +22,7 @@ prog="octavia-keepalived"
|
|||
|
||||
start() {
|
||||
echo -n $"Starting $prog"
|
||||
{{ keepalived_cmd }} -D -d -f {{ keepalived_cfg }}
|
||||
ip netns exec {{ amphora_nsname }} {{ keepalived_cmd }} -D -d -f {{ keepalived_cfg }}
|
||||
RETVAL=$?
|
||||
echo
|
||||
[ $RETVAL -eq 0 ] && touch {{ keepalived_pid }}
|
||||
|
|
|
@ -46,9 +46,19 @@ test "$ENABLED" != "0" || exit 0
|
|||
[ -f /etc/default/rcS ] && . /etc/default/rcS
|
||||
. /lib/lsb/init-functions
|
||||
|
||||
HAPROXY="ip netns exec {{ amphora_nsname }} $HAPROXY"
|
||||
|
||||
haproxy_start()
|
||||
{
|
||||
# Re-add the namespace
|
||||
ip netns add {{ amphora_nsname }} || true
|
||||
# We need the plugged_interfaces file sorted to join the host interfaces
|
||||
sort -k 1 /var/lib/octavia/plugged_interfaces > /var/lib/octavia/plugged_interfaces.sorted
|
||||
# Assign the interfaces into the namespace with the appropriate name
|
||||
ip link | awk '{getline n; print $0,n}' | awk '{sub(":","",$2)} {print $17 " " $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
|
||||
ip netns exec {{ amphora_nsname }} ifup -a || true
|
||||
|
||||
start-stop-daemon --start --pidfile "$PIDFILE" \
|
||||
--exec $HAPROXY -- -f "$CONFIG" -D -p "$PIDFILE" \
|
||||
$EXTRAOPTS || return 2
|
||||
|
|
|
@ -18,7 +18,8 @@
|
|||
|
||||
description "Properly handle haproxy"
|
||||
|
||||
start on startup
|
||||
start on runlevel [2345]
|
||||
stop on runlevel [!2345]
|
||||
|
||||
env PID_PATH={{ haproxy_pid }}
|
||||
env BIN_PATH={{ haproxy_cmd }}
|
||||
|
@ -30,15 +31,24 @@ respawn limit {{ respawn_count }} {{respawn_interval}}
|
|||
|
||||
pre-start script
|
||||
[ -r $CONF_PATH ]
|
||||
# Re-add the namespace
|
||||
ip netns add {{ amphora_nsname }} || true
|
||||
# We need the plugged_interfaces file sorted to join with the host
|
||||
# interfaces
|
||||
sort -k 1 /var/lib/octavia/plugged_interfaces > /var/lib/octavia/plugged_interfaces.sorted
|
||||
# Assign the interfaces into the namespace with the appropriate name
|
||||
ip link | awk '{getline n; print $0,n}' | awk '{sub(":","",$2)} {print $17 " " $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
|
||||
ip netns exec {{ amphora_nsname }} ifup -a || true
|
||||
end script
|
||||
|
||||
script
|
||||
exec /bin/bash <<EOF
|
||||
echo \$(date) Starting HAProxy
|
||||
# The -L trick fixes the HAProxy limitation to have long peer names
|
||||
$BIN_PATH -f $CONF_PATH -L $PEER_NAME -D -p $PID_PATH
|
||||
ip netns exec {{ amphora_nsname }} $BIN_PATH -f $CONF_PATH -L $PEER_NAME -D -p $PID_PATH
|
||||
|
||||
trap "$BIN_PATH -f $CONF_PATH -L $PEER_NAME -p $PID_PATH -sf \\\$(cat $PID_PATH)" SIGHUP
|
||||
trap "ip netns exec {{ amphora_nsname }} $BIN_PATH -f $CONF_PATH -L $PEER_NAME -p $PID_PATH -sf \\\$(cat $PID_PATH)" SIGHUP
|
||||
trap "kill -TERM \\\$(cat $PID_PATH) && rm $PID_PATH;echo \\\$(date) Exiting HAProxy; exit 0" SIGTERM SIGINT
|
||||
|
||||
while true; do # Iterate to keep job running.
|
||||
|
@ -53,4 +63,4 @@ fi
|
|||
sleep 1 # Don't sleep to long as signals will not be handled during sleep.
|
||||
done
|
||||
EOF
|
||||
end script
|
||||
end script
|
||||
|
|
|
@ -67,7 +67,8 @@ amphora_agent_opts = [
|
|||
help=_("The server certificate for the agent.py server "
|
||||
"to use")),
|
||||
cfg.StrOpt('agent_server_network_dir',
|
||||
default='/etc/network/interfaces.d/',
|
||||
default='/etc/netns/{}/network/interfaces.d/'.format(
|
||||
constants.AMPHORA_NAMESPACE),
|
||||
help=_("The directory where new network interfaces "
|
||||
"are located")),
|
||||
cfg.StrOpt('agent_server_network_file',
|
||||
|
|
|
@ -311,3 +311,5 @@ API_VERSION = '0.5'
|
|||
HAPROXY_BASE_PEER_PORT = 1025
|
||||
KEEPALIVED_CONF = 'keepalived.conf.j2'
|
||||
CHECK_SCRIPT_CONF = 'keepalived_check_script.conf.j2'
|
||||
PLUGGED_INTERFACES = '/var/lib/octavia/plugged_interfaces'
|
||||
AMPHORA_NAMESPACE = 'amphora-haproxy'
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import random
|
||||
import stat
|
||||
import subprocess
|
||||
|
||||
|
@ -487,10 +488,18 @@ class ServerTestCase(base.TestCase):
|
|||
|
||||
@mock.patch('netifaces.interfaces')
|
||||
@mock.patch('netifaces.ifaddresses')
|
||||
@mock.patch('pyroute2.IPRoute')
|
||||
@mock.patch('pyroute2.NetNS')
|
||||
@mock.patch('subprocess.check_output')
|
||||
def test_plug_network(self, mock_check_output, mock_ifaddress,
|
||||
mock_interfaces):
|
||||
def test_plug_network(self, mock_check_output, mock_netns,
|
||||
mock_pyroute2, mock_ifaddress, mock_interfaces):
|
||||
port_info = {'mac_address': '123'}
|
||||
test_int_num = random.randint(0, 9999)
|
||||
|
||||
netns_handle = mock_netns.return_value.__enter__.return_value
|
||||
netns_handle.get_links.return_value = [0] * test_int_num
|
||||
|
||||
test_int_num = str(test_int_num)
|
||||
|
||||
# No interface at all
|
||||
mock_interfaces.side_effect = [[]]
|
||||
|
@ -519,7 +528,8 @@ class ServerTestCase(base.TestCase):
|
|||
|
||||
flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC
|
||||
mode = stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH
|
||||
file_name = '/etc/network/interfaces.d/blah.cfg'
|
||||
file_name = '/etc/netns/{0}/network/interfaces.d/eth{1}.cfg'.format(
|
||||
consts.AMPHORA_NAMESPACE, test_int_num)
|
||||
m = self.useFixture(test_utils.OpenFixture(file_name)).mock_open
|
||||
with mock.patch('os.open') as mock_open, mock.patch.object(
|
||||
os, 'fdopen', m) as mock_fdopen:
|
||||
|
@ -529,15 +539,23 @@ class ServerTestCase(base.TestCase):
|
|||
data=json.dumps(port_info))
|
||||
self.assertEqual(202, rv.status_code)
|
||||
|
||||
mock_open.assert_called_with(file_name, flags, mode)
|
||||
mock_fdopen.assert_called_with(123, 'w')
|
||||
mock_open.assert_any_call(file_name, flags, mode)
|
||||
mock_fdopen.assert_any_call(123, 'w')
|
||||
|
||||
plug_inf_file = '/var/lib/octavia/plugged_interfaces'
|
||||
flags = os.O_RDWR | os.O_CREAT
|
||||
mode = stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH
|
||||
mock_open.assert_any_call(plug_inf_file, flags, mode)
|
||||
mock_fdopen.assert_any_call(123, 'r+')
|
||||
|
||||
handle = m()
|
||||
handle.write.assert_called_once_with(
|
||||
handle.write.assert_any_call(
|
||||
'\n# Generated by Octavia agent\n'
|
||||
'auto blah blah:0\n'
|
||||
'iface blah inet dhcp')
|
||||
'auto eth' + test_int_num + ' eth' + test_int_num +
|
||||
':0\niface eth' + test_int_num + ' inet dhcp')
|
||||
mock_check_output.assert_called_with(
|
||||
['ifup', 'blah'], stderr=-2)
|
||||
['ip', 'netns', 'exec', consts.AMPHORA_NAMESPACE,
|
||||
'ifup', 'eth' + test_int_num], stderr=-2)
|
||||
|
||||
# same as above but ifup fails
|
||||
mock_interfaces.side_effect = [['blah']]
|
||||
|
@ -560,10 +578,15 @@ class ServerTestCase(base.TestCase):
|
|||
|
||||
@mock.patch('netifaces.interfaces')
|
||||
@mock.patch('netifaces.ifaddresses')
|
||||
@mock.patch('subprocess.check_output')
|
||||
@mock.patch('pyroute2.IPRoute')
|
||||
def test_plug_VIP(self, mock_pyroute2, mock_check_output, mock_ifaddress,
|
||||
mock_interfaces):
|
||||
@mock.patch('pyroute2.netns.create')
|
||||
@mock.patch('pyroute2.NetNS')
|
||||
@mock.patch('subprocess.check_output')
|
||||
@mock.patch('shutil.copytree')
|
||||
@mock.patch('os.makedirs')
|
||||
def test_plug_VIP(self, mock_makedirs, mock_copytree, mock_check_output,
|
||||
mock_netns, mock_netns_create, mock_pyroute2,
|
||||
mock_ifaddress, mock_interfaces):
|
||||
|
||||
subnet_info = {'subnet_cidr': '10.0.0.0/24',
|
||||
'gateway': '10.0.0.1',
|
||||
|
@ -605,7 +628,8 @@ class ServerTestCase(base.TestCase):
|
|||
|
||||
flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC
|
||||
mode = stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH
|
||||
file_name = '/etc/network/interfaces.d/blah.cfg'
|
||||
file_name = '/etc/netns/{}/network/interfaces.d/blah.cfg'.format(
|
||||
consts.AMPHORA_NAMESPACE)
|
||||
m = self.useFixture(test_utils.OpenFixture(file_name)).mock_open
|
||||
|
||||
with mock.patch('os.open') as mock_open, mock.patch.object(
|
||||
|
@ -616,10 +640,17 @@ class ServerTestCase(base.TestCase):
|
|||
content_type='application/json',
|
||||
data=json.dumps(subnet_info))
|
||||
self.assertEqual(202, rv.status_code)
|
||||
mock_open.assert_called_with(file_name, flags, mode)
|
||||
mock_fdopen.assert_called_with(123, 'w')
|
||||
mock_open.assert_any_call(file_name, flags, mode)
|
||||
mock_fdopen.assert_any_call(123, 'w')
|
||||
|
||||
plug_inf_file = '/var/lib/octavia/plugged_interfaces'
|
||||
flags = os.O_RDWR | os.O_CREAT
|
||||
mode = stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH
|
||||
mock_open.assert_any_call(plug_inf_file, flags, mode)
|
||||
mock_fdopen.assert_any_call(123, 'r+')
|
||||
|
||||
handle = m()
|
||||
handle.write.assert_called_once_with(
|
||||
handle.write.assert_any_call(
|
||||
'\n# Generated by Octavia agent\n'
|
||||
'auto blah blah:0\n'
|
||||
'iface blah inet dhcp\n'
|
||||
|
@ -628,7 +659,8 @@ class ServerTestCase(base.TestCase):
|
|||
'broadcast 203.0.113.255\n'
|
||||
'netmask 255.255.255.0')
|
||||
mock_check_output.assert_called_with(
|
||||
['ifup', 'blah:0'], stderr=-2)
|
||||
['ip', 'netns', 'exec', consts.AMPHORA_NAMESPACE,
|
||||
'ifup', 'blah:0'], stderr=-2)
|
||||
|
||||
mock_interfaces.side_effect = [['blah']]
|
||||
mock_ifaddress.side_effect = [[netifaces.AF_LINK],
|
||||
|
@ -651,35 +683,48 @@ class ServerTestCase(base.TestCase):
|
|||
'message': 'Error plugging VIP'},
|
||||
json.loads(rv.data.decode('utf-8')))
|
||||
|
||||
@mock.patch('netifaces.ifaddresses')
|
||||
@mock.patch('netifaces.interfaces')
|
||||
def test_get_interface(self, mock_interfaces, mock_ifaddresses):
|
||||
@mock.patch('pyroute2.NetNS')
|
||||
def test_get_interface(self, mock_netns):
|
||||
|
||||
netns_handle = mock_netns.return_value.__enter__.return_value
|
||||
|
||||
interface_res = {'interface': 'eth0'}
|
||||
mock_interfaces.return_value = ['lo', 'eth0']
|
||||
mock_ifaddresses.return_value = {
|
||||
netifaces.AF_ROUTE: [{'addr': '00:00:00:00:00:00'}],
|
||||
netifaces.AF_INET: [{'addr': '203.0.113.2'}],
|
||||
netifaces.AF_INET6: [{'addr': '::1'}]}
|
||||
|
||||
# Happy path
|
||||
netns_handle.get_addr.return_value = [{
|
||||
'index': 3, 'family': 2,
|
||||
'attrs': [['IFA_ADDRESS', '203.0.113.2']]}]
|
||||
netns_handle.get_links.return_value = [{
|
||||
'attrs': [['IFLA_IFNAME', 'eth0']]}]
|
||||
rv = self.app.get('/' + api_server.VERSION + '/interface/203.0.113.2',
|
||||
data=json.dumps(interface_res),
|
||||
content_type='application/json')
|
||||
self.assertEqual(200, rv.status_code)
|
||||
|
||||
# Happy path with IPv6 address normalization
|
||||
netns_handle.get_addr.return_value = [{
|
||||
'index': 3, 'family': 10,
|
||||
'attrs': [['IFA_ADDRESS',
|
||||
'0000:0000:0000:0000:0000:0000:0000:0001']]}]
|
||||
netns_handle.get_links.return_value = [{
|
||||
'attrs': [['IFLA_IFNAME', 'eth0']]}]
|
||||
rv = self.app.get('/' + api_server.VERSION + '/interface/::1',
|
||||
data=json.dumps(interface_res),
|
||||
content_type='application/json')
|
||||
self.assertEqual(200, rv.status_code)
|
||||
|
||||
# Nonexistent interface
|
||||
rv = self.app.get('/' + api_server.VERSION + '/interface/10.0.0.1',
|
||||
data=json.dumps(interface_res),
|
||||
content_type='application/json')
|
||||
self.assertEqual(404, rv.status_code)
|
||||
|
||||
# Invalid IP address
|
||||
rv = self.app.get('/' + api_server.VERSION +
|
||||
'/interface/00:00:00:00:00:00',
|
||||
data=json.dumps(interface_res),
|
||||
content_type='application/json')
|
||||
self.assertEqual(500, rv.status_code)
|
||||
self.assertEqual(400, rv.status_code)
|
||||
|
||||
@mock.patch('os.path.exists')
|
||||
@mock.patch('os.makedirs')
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import random
|
||||
import stat
|
||||
import subprocess
|
||||
|
||||
|
@ -491,10 +492,19 @@ class ServerTestCase(base.TestCase):
|
|||
|
||||
@mock.patch('netifaces.interfaces')
|
||||
@mock.patch('netifaces.ifaddresses')
|
||||
@mock.patch('pyroute2.IPRoute')
|
||||
@mock.patch('pyroute2.NetNS')
|
||||
@mock.patch('subprocess.check_output')
|
||||
def test_plug_network(self, mock_check_output, mock_ifaddress,
|
||||
def test_plug_network(self, mock_check_output, mock_netns,
|
||||
mock_pyroute2, mock_ifaddress,
|
||||
mock_interfaces):
|
||||
port_info = {'mac_address': '123'}
|
||||
test_int_num = random.randint(0, 9999)
|
||||
|
||||
netns_handle = mock_netns.return_value.__enter__.return_value
|
||||
netns_handle.get_links.return_value = [0] * test_int_num
|
||||
|
||||
test_int_num = str(test_int_num)
|
||||
|
||||
# No interface at all
|
||||
mock_interfaces.side_effect = [[]]
|
||||
|
@ -523,7 +533,8 @@ class ServerTestCase(base.TestCase):
|
|||
|
||||
flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC
|
||||
mode = stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH
|
||||
file_name = '/etc/network/interfaces.d/blah.cfg'
|
||||
file_name = '/etc/netns/{0}/network/interfaces.d/eth{1}.cfg'.format(
|
||||
consts.AMPHORA_NAMESPACE, test_int_num)
|
||||
m = self.useFixture(test_utils.OpenFixture(file_name)).mock_open
|
||||
with mock.patch('os.open') as mock_open, mock.patch.object(
|
||||
os, 'fdopen', m) as mock_fdopen:
|
||||
|
@ -533,15 +544,24 @@ class ServerTestCase(base.TestCase):
|
|||
content_type='application/json',
|
||||
data=json.dumps(port_info))
|
||||
self.assertEqual(202, rv.status_code)
|
||||
mock_open.assert_called_with(file_name, flags, mode)
|
||||
mock_fdopen.assert_called_with(123, 'w')
|
||||
|
||||
mock_open.assert_any_call(file_name, flags, mode)
|
||||
mock_fdopen.assert_any_call(123, 'w')
|
||||
|
||||
plug_inf_file = '/var/lib/octavia/plugged_interfaces'
|
||||
flags = os.O_RDWR | os.O_CREAT
|
||||
mode = stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH
|
||||
mock_open.assert_any_call(plug_inf_file, flags, mode)
|
||||
mock_fdopen.assert_any_call(123, 'r+')
|
||||
|
||||
handle = m()
|
||||
handle.write.assert_called_once_with(
|
||||
handle.write.assert_any_call(
|
||||
'\n# Generated by Octavia agent\n'
|
||||
'auto blah blah:0\n'
|
||||
'iface blah inet dhcp')
|
||||
'auto eth' + test_int_num + ' eth' + test_int_num + ':0\n'
|
||||
'iface eth' + test_int_num + ' inet dhcp')
|
||||
mock_check_output.assert_called_with(
|
||||
['ifup', 'blah'], stderr=-2)
|
||||
['ip', 'netns', 'exec', 'amphora-haproxy', 'ifup',
|
||||
'eth' + test_int_num], stderr=-2)
|
||||
|
||||
# same as above but ifup fails
|
||||
mock_interfaces.side_effect = [['blah']]
|
||||
|
@ -564,10 +584,15 @@ class ServerTestCase(base.TestCase):
|
|||
|
||||
@mock.patch('netifaces.interfaces')
|
||||
@mock.patch('netifaces.ifaddresses')
|
||||
@mock.patch('subprocess.check_output')
|
||||
@mock.patch('pyroute2.IPRoute')
|
||||
def test_plug_VIP(self, mock_pyroute2, mock_check_output, mock_ifaddress,
|
||||
mock_interfaces):
|
||||
@mock.patch('pyroute2.netns.create')
|
||||
@mock.patch('pyroute2.NetNS')
|
||||
@mock.patch('subprocess.check_output')
|
||||
@mock.patch('shutil.copytree')
|
||||
@mock.patch('os.makedirs')
|
||||
def test_plug_VIP(self, mock_makedirs, mock_copytree, mock_check_output,
|
||||
mock_netns, mock_netns_create, mock_pyroute2,
|
||||
mock_ifaddress, mock_interfaces):
|
||||
|
||||
subnet_info = {'subnet_cidr': '10.0.0.0/24',
|
||||
'gateway': '10.0.0.1',
|
||||
|
@ -609,7 +634,8 @@ class ServerTestCase(base.TestCase):
|
|||
|
||||
flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC
|
||||
mode = stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH
|
||||
file_name = '/etc/network/interfaces.d/blah.cfg'
|
||||
file_name = '/etc/netns/{0}/network/interfaces.d/blah.cfg'.format(
|
||||
consts.AMPHORA_NAMESPACE)
|
||||
m = mock.mock_open()
|
||||
with mock.patch('os.open') as mock_open, mock.patch.object(
|
||||
os, 'fdopen', m) as mock_fdopen:
|
||||
|
@ -619,10 +645,18 @@ class ServerTestCase(base.TestCase):
|
|||
content_type='application/json',
|
||||
data=json.dumps(subnet_info))
|
||||
self.assertEqual(202, rv.status_code)
|
||||
mock_open.assert_called_with(file_name, flags, mode)
|
||||
mock_fdopen.assert_called_with(123, 'w')
|
||||
|
||||
mock_open.assert_any_call(file_name, flags, mode)
|
||||
mock_fdopen.assert_any_call(123, 'w')
|
||||
|
||||
plug_inf_file = '/var/lib/octavia/plugged_interfaces'
|
||||
flags = os.O_RDWR | os.O_CREAT
|
||||
mode = stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH
|
||||
mock_open.assert_any_call(plug_inf_file, flags, mode)
|
||||
mock_fdopen.assert_any_call(123, 'r+')
|
||||
|
||||
handle = m()
|
||||
handle.write.assert_called_once_with(
|
||||
handle.write.assert_any_call(
|
||||
'\n# Generated by Octavia agent\n'
|
||||
'auto blah blah:0\n'
|
||||
'iface blah inet dhcp\n'
|
||||
|
@ -631,7 +665,8 @@ class ServerTestCase(base.TestCase):
|
|||
'broadcast 203.0.113.255\n'
|
||||
'netmask 255.255.255.0')
|
||||
mock_check_output.assert_called_with(
|
||||
['ifup', 'blah:0'], stderr=-2)
|
||||
['ip', 'netns', 'exec', 'amphora-haproxy', 'ifup',
|
||||
'blah:0'], stderr=-2)
|
||||
|
||||
mock_interfaces.side_effect = [['blah']]
|
||||
mock_ifaddress.side_effect = [[netifaces.AF_LINK],
|
||||
|
@ -654,35 +689,48 @@ class ServerTestCase(base.TestCase):
|
|||
'message': 'Error plugging VIP'},
|
||||
json.loads(rv.data.decode('utf-8')))
|
||||
|
||||
@mock.patch('netifaces.ifaddresses')
|
||||
@mock.patch('netifaces.interfaces')
|
||||
def test_get_interface(self, mock_interfaces, mock_ifaddresses):
|
||||
@mock.patch('pyroute2.NetNS')
|
||||
def test_get_interface(self, mock_netns):
|
||||
|
||||
netns_handle = mock_netns.return_value.__enter__.return_value
|
||||
|
||||
interface_res = {'interface': 'eth0'}
|
||||
mock_interfaces.return_value = ['lo', 'eth0']
|
||||
mock_ifaddresses.return_value = {
|
||||
netifaces.AF_ROUTE: [{'addr': '00:00:00:00:00:00'}],
|
||||
netifaces.AF_INET: [{'addr': '203.0.113.2'}],
|
||||
netifaces.AF_INET6: [{'addr': '::1'}]}
|
||||
|
||||
# Happy path
|
||||
netns_handle.get_addr.return_value = [{
|
||||
'index': 3, 'family': 2,
|
||||
'attrs': [['IFA_ADDRESS', '203.0.113.2']]}]
|
||||
netns_handle.get_links.return_value = [{
|
||||
'attrs': [['IFLA_IFNAME', 'eth0']]}]
|
||||
rv = self.app.get('/' + api_server.VERSION + '/interface/203.0.113.2',
|
||||
data=json.dumps(interface_res),
|
||||
content_type='application/json')
|
||||
self.assertEqual(200, rv.status_code)
|
||||
|
||||
# Happy path with IPv6 address normalization
|
||||
netns_handle.get_addr.return_value = [{
|
||||
'index': 3, 'family': 10,
|
||||
'attrs': [['IFA_ADDRESS',
|
||||
'0000:0000:0000:0000:0000:0000:0000:0001']]}]
|
||||
netns_handle.get_links.return_value = [{
|
||||
'attrs': [['IFLA_IFNAME', 'eth0']]}]
|
||||
rv = self.app.get('/' + api_server.VERSION + '/interface/::1',
|
||||
data=json.dumps(interface_res),
|
||||
content_type='application/json')
|
||||
self.assertEqual(200, rv.status_code)
|
||||
|
||||
# Nonexistent interface
|
||||
rv = self.app.get('/' + api_server.VERSION + '/interface/10.0.0.1',
|
||||
data=json.dumps(interface_res),
|
||||
content_type='application/json')
|
||||
self.assertEqual(404, rv.status_code)
|
||||
|
||||
# Invalid IP address
|
||||
rv = self.app.get('/' + api_server.VERSION +
|
||||
'/interface/00:00:00:00:00:00',
|
||||
data=json.dumps(interface_res),
|
||||
content_type='application/json')
|
||||
self.assertEqual(500, rv.status_code)
|
||||
self.assertEqual(400, rv.status_code)
|
||||
|
||||
@mock.patch('os.path.exists')
|
||||
@mock.patch('os.makedirs')
|
||||
|
|
Loading…
Reference in New Issue