Allow IPv6 VIPs

Removes hardcoded IPv4 logic from the controller and agent.
Updates the VIP address field size in the DB.

Closes-Bug: #1585803
Closes-Bug: #1585804

Change-Id: Ib5aeef4563e20cc8ffdc607139f28aad9787aaeb
This commit is contained in:
Adam Harwell 2016-07-08 12:56:24 -07:00 committed by Michael Johnson
parent 9630ae7cb5
commit 8c50a35850
11 changed files with 430 additions and 58 deletions

View File

@ -16,7 +16,6 @@
import logging import logging
import os import os
import shutil import shutil
import socket
import stat import stat
import subprocess import subprocess
@ -37,23 +36,36 @@ from octavia.i18n import _LE, _LI
CONF = cfg.CONF CONF = cfg.CONF
CONF.import_group('amphora_agent', 'octavia.common.config') CONF.import_group('amphora_agent', 'octavia.common.config')
ETH_PORT_CONF = 'plug_vip_ethX.conf.j2' ETH_X_VIP_CONF = 'plug_vip_ethX.conf.j2'
ETH_X_PORT_CONF = 'plug_port_ethX.conf.j2'
ETH_X_VIP_CONF = 'plug_port_ethX.conf.j2'
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
j2_env = jinja2.Environment(loader=jinja2.FileSystemLoader( j2_env = jinja2.Environment(loader=jinja2.FileSystemLoader(
os.path.dirname(os.path.realpath(__file__)) + consts.AGENT_API_TEMPLATES)) os.path.dirname(os.path.realpath(__file__)) + consts.AGENT_API_TEMPLATES))
template_port = j2_env.get_template(ETH_X_VIP_CONF) template_port = j2_env.get_template(ETH_X_PORT_CONF)
template_vip = j2_env.get_template(ETH_PORT_CONF) template_vip = j2_env.get_template(ETH_X_VIP_CONF)
def plug_vip(vip, subnet_cidr, gateway, mac_address, vrrp_ip): def plug_vip(vip, subnet_cidr, gateway, mac_address, vrrp_ip=None):
# validate vip # Validate vip and subnet_cidr, calculate broadcast address and netmask
try: try:
socket.inet_aton(vip) ip = ipaddress.ip_address(
except socket.error: vip if six.text_type == type(vip) else six.u(vip))
network = ipaddress.ip_network(
subnet_cidr if six.text_type == type(subnet_cidr)
else six.u(subnet_cidr))
vip = ip.exploded
broadcast = network.broadcast_address.exploded
netmask = (network.prefixlen if ip.version is 6
else network.netmask.exploded)
vrrp_version = None
if vrrp_ip:
vrrp_ip_obj = ipaddress.ip_address(
vrrp_ip if six.text_type == type(vrrp_ip) else six.u(vrrp_ip)
)
vrrp_version = vrrp_ip_obj.version
except ValueError:
return flask.make_response(flask.jsonify(dict( return flask.make_response(flask.jsonify(dict(
message="Invalid VIP")), 400) message="Invalid VIP")), 400)
@ -61,11 +73,6 @@ def plug_vip(vip, subnet_cidr, gateway, mac_address, vrrp_ip):
primary_interface = "{interface}".format(interface=interface) primary_interface = "{interface}".format(interface=interface)
secondary_interface = "{interface}:0".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 # We need to setup the netns network directory so that the ifup
# commands used here and in the startup scripts "sees" the right # commands used here and in the startup scripts "sees" the right
# interfaces and scripts. # interfaces and scripts.
@ -104,11 +111,13 @@ def plug_vip(vip, subnet_cidr, gateway, mac_address, vrrp_ip):
text = template_vip.render( text = template_vip.render(
interface=interface, interface=interface,
vip=vip, vip=vip,
vip_ipv6=ip.version is 6,
broadcast=broadcast, broadcast=broadcast,
# assume for now only a fixed subnet size netmask=netmask,
netmask='255.255.255.0',
gateway=gateway, gateway=gateway,
vrrp_ip=vrrp_ip) vrrp_ip=vrrp_ip,
vrrp_ipv6=vrrp_version is 6,
)
text_file.write(text) text_file.write(text)
# Update the list of interfaces to add to the namespace # Update the list of interfaces to add to the namespace

View File

@ -18,16 +18,16 @@
auto {{ interface }} {{ interface }}:0 auto {{ interface }} {{ interface }}:0
{%- if vrrp_ip %} {%- if vrrp_ip %}
iface {{ interface }} inet static iface {{ interface }} inet{{ '6' if vrrp_ipv6 }} static
address {{ vrrp_ip }} address {{ vrrp_ip }}
broadcast {{ broadcast }} broadcast {{ broadcast }}
netmask {{ netmask }} netmask {{ netmask }}
gateway {{ gateway }} gateway {{ gateway }}
{%- else %} {%- else %}
iface {{ interface }} inet dhcp iface {{ interface }} inet{{ '6' if vip_ipv6 }} {{ 'auto' if vip_ipv6 else 'dhcp' }}
{%- endif %} {%- endif %}
iface {{ interface }}:0 inet static iface {{ interface }}:0 inet{{ '6' if vip_ipv6 }} static
address {{ vip }} address {{ vip }}
broadcast {{ broadcast }} broadcast {{ broadcast }}
netmask {{ netmask }} netmask {{ netmask }}

View File

@ -0,0 +1,33 @@
# 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.
"""Update vip address size
Revision ID: 82b9402e71fd
Revises: 62816c232310
Create Date: 2016-07-17 14:36:36.698870
"""
# revision identifiers, used by Alembic.
revision = '82b9402e71fd'
down_revision = '4a6ec0ab7284'
from alembic import op
import sqlalchemy as sa
def upgrade():
op.alter_column(u'vip', u'ip_address',
existing_type=sa.String(64))

View File

@ -316,7 +316,7 @@ class Vip(base_models.BASE):
sa.ForeignKey("load_balancer.id", sa.ForeignKey("load_balancer.id",
name="fk_vip_load_balancer_id"), name="fk_vip_load_balancer_id"),
nullable=False, primary_key=True) nullable=False, primary_key=True)
ip_address = sa.Column(sa.String(36), nullable=True) ip_address = sa.Column(sa.String(64), nullable=True)
port_id = sa.Column(sa.String(36), nullable=True) port_id = sa.Column(sa.String(36), nullable=True)
subnet_id = sa.Column(sa.String(36), nullable=True) subnet_id = sa.Column(sa.String(36), nullable=True)
load_balancer = orm.relationship("LoadBalancer", uselist=False, load_balancer = orm.relationship("LoadBalancer", uselist=False,

View File

@ -29,6 +29,8 @@ from octavia.network import data_models as n_data_models
from octavia.network.drivers.neutron import base as neutron_base from octavia.network.drivers.neutron import base as neutron_base
from octavia.network.drivers.neutron import utils from octavia.network.drivers.neutron import utils
import ipaddress
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
AAP_EXT_ALIAS = 'allowed-address-pairs' AAP_EXT_ALIAS = 'allowed-address-pairs'
@ -111,6 +113,11 @@ class AllowedAddressPairsDriver(neutron_base.BaseNeutronDriver):
if sec_grps and sec_grps.get('security_groups'): if sec_grps and sec_grps.get('security_groups'):
return sec_grps.get('security_groups')[0] return sec_grps.get('security_groups')[0]
def _get_ethertype_for_ip(self, ip):
address = ipaddress.ip_address(
ip if six.text_type == type(ip) else six.u(ip))
return 'IPv6' if address.version is 6 else 'IPv4'
def _update_security_group_rules(self, load_balancer, sec_grp_id): def _update_security_group_rules(self, load_balancer, sec_grp_id):
rules = self.neutron_client.list_security_group_rules( rules = self.neutron_client.list_security_group_rules(
security_group_id=sec_grp_id) security_group_id=sec_grp_id)
@ -140,9 +147,11 @@ class AllowedAddressPairsDriver(neutron_base.BaseNeutronDriver):
if rule.get('port_range_max') in del_ports: if rule.get('port_range_max') in del_ports:
self.neutron_client.delete_security_group_rule(rule.get('id')) self.neutron_client.delete_security_group_rule(rule.get('id'))
ethertype = self._get_ethertype_for_ip(load_balancer.vip.ip_address)
for port in add_ports: for port in add_ports:
self._create_security_group_rule(sec_grp_id, 'TCP', port_min=port, self._create_security_group_rule(sec_grp_id, 'TCP', port_min=port,
port_max=port) port_max=port,
ethertype=ethertype)
# Currently we are using the VIP network for VRRP # Currently we are using the VIP network for VRRP
# so we need to open up the protocols for it # so we need to open up the protocols for it
@ -152,7 +161,8 @@ class AllowedAddressPairsDriver(neutron_base.BaseNeutronDriver):
self._create_security_group_rule( self._create_security_group_rule(
sec_grp_id, sec_grp_id,
constants.VRRP_PROTOCOL_NUM, constants.VRRP_PROTOCOL_NUM,
direction='ingress') direction='ingress',
ethertype=ethertype)
except neutron_client_exceptions.Conflict: except neutron_client_exceptions.Conflict:
# It's ok if this rule already exists # It's ok if this rule already exists
pass pass
@ -162,7 +172,7 @@ class AllowedAddressPairsDriver(neutron_base.BaseNeutronDriver):
try: try:
self._create_security_group_rule( self._create_security_group_rule(
sec_grp_id, constants.AUTH_HEADER_PROTOCOL_NUMBER, sec_grp_id, constants.AUTH_HEADER_PROTOCOL_NUMBER,
direction='ingress') direction='ingress', ethertype=ethertype)
except neutron_client_exceptions.Conflict: except neutron_client_exceptions.Conflict:
# It's ok if this rule already exists # It's ok if this rule already exists
pass pass

View File

@ -120,14 +120,15 @@ class BaseNeutronDriver(base.AbstractNetworkDriver):
def _create_security_group_rule(self, sec_grp_id, protocol, def _create_security_group_rule(self, sec_grp_id, protocol,
direction='ingress', port_min=None, direction='ingress', port_min=None,
port_max=None): port_max=None, ethertype='IPv6'):
rule = { rule = {
'security_group_rule': { 'security_group_rule': {
'security_group_id': sec_grp_id, 'security_group_id': sec_grp_id,
'direction': direction, 'direction': direction,
'protocol': protocol, 'protocol': protocol,
'port_range_min': port_min, 'port_range_min': port_min,
'port_range_max': port_max 'port_range_max': port_max,
'ethertype': ethertype,
} }
} }
self.neutron_client.create_security_group_rule(rule) self.neutron_client.create_security_group_rule(rule)

View File

@ -690,15 +690,17 @@ class ServerTestCase(base.TestCase):
@mock.patch('subprocess.check_output') @mock.patch('subprocess.check_output')
@mock.patch('shutil.copytree') @mock.patch('shutil.copytree')
@mock.patch('os.makedirs') @mock.patch('os.makedirs')
def test_plug_VIP(self, mock_makedirs, mock_copytree, mock_check_output, def test_plug_vip4(self, mock_makedirs, mock_copytree, mock_check_output,
mock_netns, mock_netns_create, mock_pyroute2, mock_netns, mock_netns_create, mock_pyroute2,
mock_ifaddress, mock_interfaces): mock_ifaddress, mock_interfaces):
subnet_info = {'subnet_cidr': '10.0.0.0/24', subnet_info = {
'gateway': '10.0.0.1', 'subnet_cidr': '203.0.113.0/24',
'mac_address': '123'} 'gateway': '203.0.113.1',
'mac_address': '123'
}
# malformated ip # malformed ip
rv = self.app.post('/' + api_server.VERSION + '/plug/vip/error', rv = self.app.post('/' + api_server.VERSION + '/plug/vip/error',
data=json.dumps(subnet_info), data=json.dumps(subnet_info),
content_type='application/json') content_type='application/json')
@ -727,7 +729,7 @@ class ServerTestCase(base.TestCase):
self.assertEqual(dict(details="No suitable network interface found"), self.assertEqual(dict(details="No suitable network interface found"),
json.loads(rv.data.decode('utf-8'))) json.loads(rv.data.decode('utf-8')))
# One Interface down, Happy Path # One Interface down, Happy Path IPv4
mock_interfaces.side_effect = [['blah']] mock_interfaces.side_effect = [['blah']]
mock_ifaddress.side_effect = [[netifaces.AF_LINK], mock_ifaddress.side_effect = [[netifaces.AF_LINK],
{netifaces.AF_LINK: [{'addr': '123'}]}] {netifaces.AF_LINK: [{'addr': '123'}]}]
@ -789,6 +791,114 @@ class ServerTestCase(base.TestCase):
'message': 'Error plugging VIP'}, 'message': 'Error plugging VIP'},
json.loads(rv.data.decode('utf-8'))) json.loads(rv.data.decode('utf-8')))
@mock.patch('netifaces.interfaces')
@mock.patch('netifaces.ifaddresses')
@mock.patch('pyroute2.IPRoute')
@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_vip6(self, mock_makedirs, mock_copytree, mock_check_output,
mock_netns, mock_netns_create, mock_pyroute2,
mock_ifaddress, mock_interfaces):
subnet_info = {
'subnet_cidr': '2001:db8::/32',
'gateway': '2001:db8::1',
'mac_address': '123'
}
# malformed ip
rv = self.app.post('/' + api_server.VERSION + '/plug/vip/error',
data=json.dumps(subnet_info),
content_type='application/json')
self.assertEqual(400, rv.status_code)
# No subnet info
rv = self.app.post('/' + api_server.VERSION + '/plug/vip/error')
self.assertEqual(400, rv.status_code)
# No interface at all
mock_interfaces.side_effect = [[]]
rv = self.app.post('/' + api_server.VERSION + "/plug/vip/2001:db8::2",
content_type='application/json',
data=json.dumps(subnet_info))
self.assertEqual(404, rv.status_code)
self.assertEqual(dict(details="No suitable network interface found"),
json.loads(rv.data.decode('utf-8')))
# Two interfaces down
mock_interfaces.side_effect = [['blah', 'blah2']]
mock_ifaddress.side_effect = [['blabla'], ['blabla']]
rv = self.app.post('/' + api_server.VERSION + "/plug/vip/2001:db8::2",
content_type='application/json',
data=json.dumps(subnet_info))
self.assertEqual(404, rv.status_code)
self.assertEqual(dict(details="No suitable network interface found"),
json.loads(rv.data.decode('utf-8')))
# One Interface down, Happy Path IPv6
mock_interfaces.side_effect = [['blah']]
mock_ifaddress.side_effect = [[netifaces.AF_LINK],
{netifaces.AF_LINK: [{'addr': '123'}]}]
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/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(
os, 'fdopen', m) as mock_fdopen:
mock_open.return_value = 123
rv = self.app.post('/' + api_server.VERSION +
"/plug/vip/2001:db8::2",
content_type='application/json',
data=json.dumps(subnet_info))
self.assertEqual(202, rv.status_code)
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_any_call(
'\n# Generated by Octavia agent\n'
'auto blah blah:0\n'
'iface blah inet6 auto\n\n'
'iface blah:0 inet6 static\n'
'address 2001:0db8:0000:0000:0000:0000:0000:0002\n'
'broadcast 2001:0db8:ffff:ffff:ffff:ffff:ffff:ffff\n'
'netmask 32')
mock_check_output.assert_called_with(
['ip', 'netns', 'exec', consts.AMPHORA_NAMESPACE,
'ifup', 'blah:0'], stderr=-2)
mock_interfaces.side_effect = [['blah']]
mock_ifaddress.side_effect = [[netifaces.AF_LINK],
{netifaces.AF_LINK: [{'addr': '123'}]}]
mock_check_output.side_effect = [
'unplug1',
subprocess.CalledProcessError(
7, 'test', RANDOM_ERROR), subprocess.CalledProcessError(
7, 'test', RANDOM_ERROR)]
m = self.useFixture(test_utils.OpenFixture(file_name)).mock_open
with mock.patch('os.open'), mock.patch.object(os, 'fdopen', m):
rv = self.app.post('/' + api_server.VERSION +
"/plug/vip/2001:db8::2",
content_type='application/json',
data=json.dumps(subnet_info))
self.assertEqual(500, rv.status_code)
self.assertEqual(
{'details': RANDOM_ERROR,
'message': 'Error plugging VIP'},
json.loads(rv.data.decode('utf-8')))
@mock.patch('pyroute2.NetNS') @mock.patch('pyroute2.NetNS')
def test_get_interface(self, mock_netns): def test_get_interface(self, mock_netns):

View File

@ -593,15 +593,17 @@ class ServerTestCase(base.TestCase):
@mock.patch('subprocess.check_output') @mock.patch('subprocess.check_output')
@mock.patch('shutil.copytree') @mock.patch('shutil.copytree')
@mock.patch('os.makedirs') @mock.patch('os.makedirs')
def test_plug_VIP(self, mock_makedirs, mock_copytree, mock_check_output, def test_plug_vip4(self, mock_makedirs, mock_copytree, mock_check_output,
mock_netns, mock_netns_create, mock_pyroute2, mock_netns, mock_netns_create, mock_pyroute2,
mock_ifaddress, mock_interfaces): mock_ifaddress, mock_interfaces):
subnet_info = {'subnet_cidr': '10.0.0.0/24', subnet_info = {
'gateway': '10.0.0.1', 'subnet_cidr': '203.0.113.0/24',
'mac_address': '123'} 'gateway': '203.0.113.1',
'mac_address': '123'
}
# malformated ip # malformed ip
rv = self.app.post('/' + api_server.VERSION + '/plug/vip/error', rv = self.app.post('/' + api_server.VERSION + '/plug/vip/error',
data=json.dumps(subnet_info), data=json.dumps(subnet_info),
content_type='application/json') content_type='application/json')
@ -630,7 +632,7 @@ class ServerTestCase(base.TestCase):
self.assertEqual(dict(details="No suitable network interface found"), self.assertEqual(dict(details="No suitable network interface found"),
json.loads(rv.data.decode('utf-8'))) json.loads(rv.data.decode('utf-8')))
# One Interface down, Happy Path # One Interface down, Happy Path IPv4
mock_interfaces.side_effect = [['blah']] mock_interfaces.side_effect = [['blah']]
mock_ifaddress.side_effect = [[netifaces.AF_LINK], mock_ifaddress.side_effect = [[netifaces.AF_LINK],
{netifaces.AF_LINK: [{'addr': '123'}]}] {netifaces.AF_LINK: [{'addr': '123'}]}]
@ -692,6 +694,115 @@ class ServerTestCase(base.TestCase):
'message': 'Error plugging VIP'}, 'message': 'Error plugging VIP'},
json.loads(rv.data.decode('utf-8'))) json.loads(rv.data.decode('utf-8')))
@mock.patch('netifaces.interfaces')
@mock.patch('netifaces.ifaddresses')
@mock.patch('pyroute2.IPRoute')
@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_vip6(self, mock_makedirs, mock_copytree, mock_check_output,
mock_netns, mock_netns_create, mock_pyroute2,
mock_ifaddress, mock_interfaces):
subnet_info = {
'subnet_cidr': '2001:db8::/32',
'gateway': '2001:db8::1',
'mac_address': '123'
}
# malformed ip
rv = self.app.post('/' + api_server.VERSION + '/plug/vip/error',
data=json.dumps(subnet_info),
content_type='application/json')
self.assertEqual(400, rv.status_code)
# No subnet info
rv = self.app.post('/' + api_server.VERSION + '/plug/vip/error')
self.assertEqual(400, rv.status_code)
# No interface at all
mock_interfaces.side_effect = [[]]
rv = self.app.post('/' + api_server.VERSION + "/plug/vip/2001:db8::2",
content_type='application/json',
data=json.dumps(subnet_info))
self.assertEqual(404, rv.status_code)
self.assertEqual(dict(details="No suitable network interface found"),
json.loads(rv.data.decode('utf-8')))
# Two interfaces down
mock_interfaces.side_effect = [['blah', 'blah2']]
mock_ifaddress.side_effect = [['blabla'], ['blabla']]
rv = self.app.post('/' + api_server.VERSION + "/plug/vip/2001:db8::2",
content_type='application/json',
data=json.dumps(subnet_info))
self.assertEqual(404, rv.status_code)
self.assertEqual(dict(details="No suitable network interface found"),
json.loads(rv.data.decode('utf-8')))
# One Interface down, Happy Path IPv6
mock_interfaces.side_effect = [['blah']]
mock_ifaddress.side_effect = [[netifaces.AF_LINK],
{netifaces.AF_LINK: [{'addr': '123'}]}]
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/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:
mock_open.return_value = 123
rv = self.app.post('/' + api_server.VERSION +
"/plug/vip/2001:db8::2",
content_type='application/json',
data=json.dumps(subnet_info))
self.assertEqual(202, rv.status_code)
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_any_call(
'\n# Generated by Octavia agent\n'
'auto blah blah:0\n'
'iface blah inet6 auto\n\n'
'iface blah:0 inet6 static\n'
'address 2001:0db8:0000:0000:0000:0000:0000:0002\n'
'broadcast 2001:0db8:ffff:ffff:ffff:ffff:ffff:ffff\n'
'netmask 32')
mock_check_output.assert_called_with(
['ip', 'netns', 'exec', 'amphora-haproxy', 'ifup',
'blah:0'], stderr=-2)
mock_interfaces.side_effect = [['blah']]
mock_ifaddress.side_effect = [[netifaces.AF_LINK],
{netifaces.AF_LINK: [{'addr': '123'}]}]
mock_check_output.side_effect = [
'unplug1',
subprocess.CalledProcessError(
7, 'test', RANDOM_ERROR), subprocess.CalledProcessError(
7, 'test', RANDOM_ERROR)]
m = mock.mock_open()
with mock.patch('os.open'), mock.patch.object(os, 'fdopen', m):
rv = self.app.post('/' + api_server.VERSION +
"/plug/vip/2001:db8::2",
content_type='application/json',
data=json.dumps(subnet_info))
self.assertEqual(500, rv.status_code)
self.assertEqual(
{'details': RANDOM_ERROR,
'message': 'Error plugging VIP'},
json.loads(rv.data.decode('utf-8')))
@mock.patch('pyroute2.NetNS') @mock.patch('pyroute2.NetNS')
def test_get_interface(self, mock_netns): def test_get_interface(self, mock_netns):

View File

@ -12,24 +12,109 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
import os
import mock import mock
import netifaces import netifaces
from octavia.amphorae.backends.agent.api_server import plug from octavia.amphorae.backends.agent.api_server import plug
import octavia.tests.unit.base as base import octavia.tests.unit.base as base
FAKE_CIDR_IPV4 = '10.0.0.0/24'
FAKE_GATEWAY_IPV4 = '10.0.0.1'
FAKE_IP_IPV4 = '10.0.0.2'
FAKE_CIDR_IPV6 = '2001:db8::/32'
FAKE_GATEWAY_IPV6 = '2001:db8::1'
FAKE_IP_IPV6 = '2001:db8::2'
FAKE_IP_IPV6_EXPANDED = '2001:0db8:0000:0000:0000:0000:0000:0002'
FAKE_MAC_ADDRESS = 'ab:cd:ef:00:ff:22'
FAKE_INTERFACE = 'eth0'
@mock.patch.object(plug, "netifaces")
class TestPlug(base.TestCase): class TestPlug(base.TestCase):
def setUp(self):
super(TestPlug, self).setUp()
def test__interface_by_mac_case_insensitive(self, mock_netifaces): self.mock_netifaces = mock.patch.object(plug, "netifaces").start()
mock_netifaces.AF_LINK = netifaces.AF_LINK self.addCleanup(self.mock_netifaces.stop)
mock_interface = 'eth0'
mock_netifaces.interfaces.return_value = [mock_interface] # Set up our fake interface
mock_netifaces.ifaddresses.return_value = { self.mock_netifaces.AF_LINK = netifaces.AF_LINK
self.mock_netifaces.interfaces.return_value = [FAKE_INTERFACE]
self.mock_netifaces.ifaddresses.return_value = {
netifaces.AF_LINK: [ netifaces.AF_LINK: [
{'addr': 'ab:cd:ef:00:ff:22'} {'addr': FAKE_MAC_ADDRESS.lower()}
] ]
} }
interface = plug._interface_by_mac('AB:CD:EF:00:FF:22')
self.assertEqual('eth0', interface) def test__interface_by_mac_case_insensitive(self):
interface = plug._interface_by_mac(FAKE_MAC_ADDRESS.upper())
self.assertEqual(FAKE_INTERFACE, interface)
@mock.patch.object(plug, "flask")
@mock.patch('pyroute2.IPRoute')
@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_ipv4(self, mock_makedirs, mock_copytree,
mock_check_output, mock_netns, mock_netns_create,
mock_pyroute2, mock_flask):
m = mock.mock_open()
with mock.patch('os.open'), mock.patch.object(os, 'fdopen', m):
plug.plug_vip(
vip=FAKE_IP_IPV4,
subnet_cidr=FAKE_CIDR_IPV4,
gateway=FAKE_GATEWAY_IPV4,
mac_address=FAKE_MAC_ADDRESS
)
mock_flask.jsonify.assert_any_call({
'message': 'OK',
'details': 'VIP {vip} plugged on interface {interface}'.format(
vip=FAKE_IP_IPV4, interface=FAKE_INTERFACE)
})
@mock.patch.object(plug, "flask")
@mock.patch('pyroute2.IPRoute')
@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_ipv6(self, mock_makedirs, mock_copytree,
mock_check_output, mock_netns, mock_netns_create,
mock_pyroute2, mock_flask):
m = mock.mock_open()
with mock.patch('os.open'), mock.patch.object(os, 'fdopen', m):
plug.plug_vip(
vip=FAKE_IP_IPV6,
subnet_cidr=FAKE_CIDR_IPV6,
gateway=FAKE_GATEWAY_IPV6,
mac_address=FAKE_MAC_ADDRESS
)
mock_flask.jsonify.assert_any_call({
'message': 'OK',
'details': 'VIP {vip} plugged on interface {interface}'.format(
vip=FAKE_IP_IPV6_EXPANDED, interface=FAKE_INTERFACE)
})
@mock.patch.object(plug, "flask")
@mock.patch('pyroute2.IPRoute')
@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_bad_ip(self, mock_makedirs, mock_copytree,
mock_check_output, mock_netns, mock_netns_create,
mock_pyroute2, mock_flask):
m = mock.mock_open()
with mock.patch('os.open'), mock.patch.object(os, 'fdopen', m):
plug.plug_vip(
vip="error",
subnet_cidr=FAKE_CIDR_IPV4,
gateway=FAKE_GATEWAY_IPV4,
mac_address=FAKE_MAC_ADDRESS
)
mock_flask.jsonify.assert_any_call({'message': 'Invalid VIP'})

View File

@ -457,7 +457,8 @@ class TestAllowedAddressPairsDriver(base.TestCase):
def test_update_vip(self): def test_update_vip(self):
listeners = [data_models.Listener(protocol_port=80, peer_port=1024), listeners = [data_models.Listener(protocol_port=80, peer_port=1024),
data_models.Listener(protocol_port=443, peer_port=1025)] data_models.Listener(protocol_port=443, peer_port=1025)]
lb = data_models.LoadBalancer(id='1', listeners=listeners) vip = data_models.Vip(ip_address='10.0.0.2')
lb = data_models.LoadBalancer(id='1', listeners=listeners, vip=vip)
list_sec_grps = self.driver.neutron_client.list_security_groups list_sec_grps = self.driver.neutron_client.list_security_groups
list_sec_grps.return_value = {'security_groups': [{'id': 'secgrp-1'}]} list_sec_grps.return_value = {'security_groups': [{'id': 'secgrp-1'}]}
fake_rules = { fake_rules = {
@ -478,7 +479,8 @@ class TestAllowedAddressPairsDriver(base.TestCase):
'direction': 'ingress', 'direction': 'ingress',
'protocol': 'TCP', 'protocol': 'TCP',
'port_range_min': 1024, 'port_range_min': 1024,
'port_range_max': 1024 'port_range_max': 1024,
'ethertype': 'IPv4'
} }
} }
expected_create_rule_2 = { expected_create_rule_2 = {
@ -487,7 +489,8 @@ class TestAllowedAddressPairsDriver(base.TestCase):
'direction': 'ingress', 'direction': 'ingress',
'protocol': 'TCP', 'protocol': 'TCP',
'port_range_min': 1025, 'port_range_min': 1025,
'port_range_max': 1025 'port_range_max': 1025,
'ethertype': 'IPv4'
} }
} }
expected_create_rule_3 = { expected_create_rule_3 = {
@ -496,7 +499,8 @@ class TestAllowedAddressPairsDriver(base.TestCase):
'direction': 'ingress', 'direction': 'ingress',
'protocol': 'TCP', 'protocol': 'TCP',
'port_range_min': 443, 'port_range_min': 443,
'port_range_max': 443 'port_range_max': 443,
'ethertype': 'IPv4'
} }
} }
create_rule.assert_has_calls([mock.call(expected_create_rule_1), create_rule.assert_has_calls([mock.call(expected_create_rule_1),
@ -508,7 +512,8 @@ class TestAllowedAddressPairsDriver(base.TestCase):
data_models.Listener( data_models.Listener(
protocol_port=443, protocol_port=443,
provisioning_status=constants.PENDING_DELETE)] provisioning_status=constants.PENDING_DELETE)]
lb = data_models.LoadBalancer(id='1', listeners=listeners) vip = data_models.Vip(ip_address='10.0.0.2')
lb = data_models.LoadBalancer(id='1', listeners=listeners, vip=vip)
list_sec_grps = self.driver.neutron_client.list_security_groups list_sec_grps = self.driver.neutron_client.list_security_groups
list_sec_grps.return_value = {'security_groups': [{'id': 'secgrp-1'}]} list_sec_grps.return_value = {'security_groups': [{'id': 'secgrp-1'}]}
fake_rules = { fake_rules = {
@ -527,7 +532,8 @@ class TestAllowedAddressPairsDriver(base.TestCase):
def test_update_vip_when_no_listeners(self): def test_update_vip_when_no_listeners(self):
listeners = [] listeners = []
lb = data_models.LoadBalancer(id='1', listeners=listeners) vip = data_models.Vip(ip_address='10.0.0.2')
lb = data_models.LoadBalancer(id='1', listeners=listeners, vip=vip)
list_sec_grps = self.driver.neutron_client.list_security_groups list_sec_grps = self.driver.neutron_client.list_security_groups
list_sec_grps.return_value = {'security_groups': [{'id': 'secgrp-1'}]} list_sec_grps.return_value = {'security_groups': [{'id': 'secgrp-1'}]}
fake_rules = { fake_rules = {

View File

@ -0,0 +1,7 @@
---
features:
- Adds support for IPv6
upgrade:
- To support IPv6 a databse migration and amphora image update are required.
fixes:
- Resolves an issue with subnets larger than /24