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:
parent
9630ae7cb5
commit
8c50a35850
@ -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
|
||||
|
@ -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 }}
|
||||
|
@ -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))
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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):
|
||||
|
||||
|
@ -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):
|
||||
|
||||
|
@ -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'})
|
||||
|
@ -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 = {
|
||||
|
7
releasenotes/notes/IPv6-support-953ef81ed8555fce.yaml
Normal file
7
releasenotes/notes/IPv6-support-953ef81ed8555fce.yaml
Normal 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
|
Loading…
Reference in New Issue
Block a user