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 os
import shutil
import socket
import stat
import subprocess
@ -37,23 +36,36 @@ from octavia.i18n import _LE, _LI
CONF = cfg.CONF
CONF.import_group('amphora_agent', 'octavia.common.config')
ETH_PORT_CONF = 'plug_vip_ethX.conf.j2'
ETH_X_VIP_CONF = 'plug_port_ethX.conf.j2'
ETH_X_VIP_CONF = 'plug_vip_ethX.conf.j2'
ETH_X_PORT_CONF = 'plug_port_ethX.conf.j2'
LOG = logging.getLogger(__name__)
j2_env = jinja2.Environment(loader=jinja2.FileSystemLoader(
os.path.dirname(os.path.realpath(__file__)) + consts.AGENT_API_TEMPLATES))
template_port = j2_env.get_template(ETH_X_VIP_CONF)
template_vip = j2_env.get_template(ETH_PORT_CONF)
template_port = j2_env.get_template(ETH_X_PORT_CONF)
template_vip = j2_env.get_template(ETH_X_VIP_CONF)
def plug_vip(vip, subnet_cidr, gateway, mac_address, vrrp_ip):
# validate vip
def plug_vip(vip, subnet_cidr, gateway, mac_address, vrrp_ip=None):
# Validate vip and subnet_cidr, calculate broadcast address and netmask
try:
socket.inet_aton(vip)
except socket.error:
ip = ipaddress.ip_address(
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(
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)
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.
@ -104,11 +111,13 @@ def plug_vip(vip, subnet_cidr, gateway, mac_address, vrrp_ip):
text = template_vip.render(
interface=interface,
vip=vip,
vip_ipv6=ip.version is 6,
broadcast=broadcast,
# assume for now only a fixed subnet size
netmask='255.255.255.0',
netmask=netmask,
gateway=gateway,
vrrp_ip=vrrp_ip)
vrrp_ip=vrrp_ip,
vrrp_ipv6=vrrp_version is 6,
)
text_file.write(text)
# Update the list of interfaces to add to the namespace

View File

@ -18,16 +18,16 @@
auto {{ interface }} {{ interface }}:0
{%- if vrrp_ip %}
iface {{ interface }} inet static
iface {{ interface }} inet{{ '6' if vrrp_ipv6 }} static
address {{ vrrp_ip }}
broadcast {{ broadcast }}
netmask {{ netmask }}
gateway {{ gateway }}
{%- else %}
iface {{ interface }} inet dhcp
iface {{ interface }} inet{{ '6' if vip_ipv6 }} {{ 'auto' if vip_ipv6 else 'dhcp' }}
{%- endif %}
iface {{ interface }}:0 inet static
iface {{ interface }}:0 inet{{ '6' if vip_ipv6 }} static
address {{ vip }}
broadcast {{ broadcast }}
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",
name="fk_vip_load_balancer_id"),
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)
subnet_id = sa.Column(sa.String(36), nullable=True)
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 utils
import ipaddress
LOG = logging.getLogger(__name__)
AAP_EXT_ALIAS = 'allowed-address-pairs'
@ -111,6 +113,11 @@ class AllowedAddressPairsDriver(neutron_base.BaseNeutronDriver):
if sec_grps and sec_grps.get('security_groups'):
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):
rules = self.neutron_client.list_security_group_rules(
security_group_id=sec_grp_id)
@ -140,9 +147,11 @@ class AllowedAddressPairsDriver(neutron_base.BaseNeutronDriver):
if rule.get('port_range_max') in del_ports:
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:
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
# so we need to open up the protocols for it
@ -152,7 +161,8 @@ class AllowedAddressPairsDriver(neutron_base.BaseNeutronDriver):
self._create_security_group_rule(
sec_grp_id,
constants.VRRP_PROTOCOL_NUM,
direction='ingress')
direction='ingress',
ethertype=ethertype)
except neutron_client_exceptions.Conflict:
# It's ok if this rule already exists
pass
@ -162,7 +172,7 @@ class AllowedAddressPairsDriver(neutron_base.BaseNeutronDriver):
try:
self._create_security_group_rule(
sec_grp_id, constants.AUTH_HEADER_PROTOCOL_NUMBER,
direction='ingress')
direction='ingress', ethertype=ethertype)
except neutron_client_exceptions.Conflict:
# It's ok if this rule already exists
pass

View File

@ -120,14 +120,15 @@ class BaseNeutronDriver(base.AbstractNetworkDriver):
def _create_security_group_rule(self, sec_grp_id, protocol,
direction='ingress', port_min=None,
port_max=None):
port_max=None, ethertype='IPv6'):
rule = {
'security_group_rule': {
'security_group_id': sec_grp_id,
'direction': direction,
'protocol': protocol,
'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)

View File

@ -690,15 +690,17 @@ class ServerTestCase(base.TestCase):
@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):
def test_plug_vip4(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',
'mac_address': '123'}
subnet_info = {
'subnet_cidr': '203.0.113.0/24',
'gateway': '203.0.113.1',
'mac_address': '123'
}
# malformated ip
# malformed ip
rv = self.app.post('/' + api_server.VERSION + '/plug/vip/error',
data=json.dumps(subnet_info),
content_type='application/json')
@ -727,7 +729,7 @@ class ServerTestCase(base.TestCase):
self.assertEqual(dict(details="No suitable network interface found"),
json.loads(rv.data.decode('utf-8')))
# One Interface down, Happy Path
# One Interface down, Happy Path IPv4
mock_interfaces.side_effect = [['blah']]
mock_ifaddress.side_effect = [[netifaces.AF_LINK],
{netifaces.AF_LINK: [{'addr': '123'}]}]
@ -789,6 +791,114 @@ class ServerTestCase(base.TestCase):
'message': 'Error plugging VIP'},
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')
def test_get_interface(self, mock_netns):

View File

@ -593,15 +593,17 @@ class ServerTestCase(base.TestCase):
@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):
def test_plug_vip4(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',
'mac_address': '123'}
subnet_info = {
'subnet_cidr': '203.0.113.0/24',
'gateway': '203.0.113.1',
'mac_address': '123'
}
# malformated ip
# malformed ip
rv = self.app.post('/' + api_server.VERSION + '/plug/vip/error',
data=json.dumps(subnet_info),
content_type='application/json')
@ -630,7 +632,7 @@ class ServerTestCase(base.TestCase):
self.assertEqual(dict(details="No suitable network interface found"),
json.loads(rv.data.decode('utf-8')))
# One Interface down, Happy Path
# One Interface down, Happy Path IPv4
mock_interfaces.side_effect = [['blah']]
mock_ifaddress.side_effect = [[netifaces.AF_LINK],
{netifaces.AF_LINK: [{'addr': '123'}]}]
@ -692,6 +694,115 @@ class ServerTestCase(base.TestCase):
'message': 'Error plugging VIP'},
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')
def test_get_interface(self, mock_netns):

View File

@ -12,24 +12,109 @@
# License for the specific language governing permissions and limitations
# under the License.
import os
import mock
import netifaces
from octavia.amphorae.backends.agent.api_server import plug
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):
def setUp(self):
super(TestPlug, self).setUp()
def test__interface_by_mac_case_insensitive(self, mock_netifaces):
mock_netifaces.AF_LINK = netifaces.AF_LINK
mock_interface = 'eth0'
mock_netifaces.interfaces.return_value = [mock_interface]
mock_netifaces.ifaddresses.return_value = {
self.mock_netifaces = mock.patch.object(plug, "netifaces").start()
self.addCleanup(self.mock_netifaces.stop)
# Set up our fake interface
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: [
{'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):
listeners = [data_models.Listener(protocol_port=80, peer_port=1024),
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.return_value = {'security_groups': [{'id': 'secgrp-1'}]}
fake_rules = {
@ -478,7 +479,8 @@ class TestAllowedAddressPairsDriver(base.TestCase):
'direction': 'ingress',
'protocol': 'TCP',
'port_range_min': 1024,
'port_range_max': 1024
'port_range_max': 1024,
'ethertype': 'IPv4'
}
}
expected_create_rule_2 = {
@ -487,7 +489,8 @@ class TestAllowedAddressPairsDriver(base.TestCase):
'direction': 'ingress',
'protocol': 'TCP',
'port_range_min': 1025,
'port_range_max': 1025
'port_range_max': 1025,
'ethertype': 'IPv4'
}
}
expected_create_rule_3 = {
@ -496,7 +499,8 @@ class TestAllowedAddressPairsDriver(base.TestCase):
'direction': 'ingress',
'protocol': 'TCP',
'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),
@ -508,7 +512,8 @@ class TestAllowedAddressPairsDriver(base.TestCase):
data_models.Listener(
protocol_port=443,
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.return_value = {'security_groups': [{'id': 'secgrp-1'}]}
fake_rules = {
@ -527,7 +532,8 @@ class TestAllowedAddressPairsDriver(base.TestCase):
def test_update_vip_when_no_listeners(self):
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.return_value = {'security_groups': [{'id': 'secgrp-1'}]}
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