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:
Michael Johnson 2016-04-01 03:16:07 +00:00
parent 5d5dcf9951
commit b89abe1871
13 changed files with 350 additions and 139 deletions

View File

@ -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

View File

@ -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 =

View 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 "

View File

@ -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)

View File

@ -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)

View File

@ -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))

View File

@ -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 }}

View File

@ -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

View File

@ -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

View File

@ -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',

View 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'

View File

@ -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')

View File

@ -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')