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 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
|
||||||
|
@ -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 }}
|
||||||
|
@ -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",
|
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,
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
@ -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):
|
||||||
|
|
||||||
|
@ -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):
|
||||||
|
|
||||||
|
@ -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'})
|
||||||
|
@ -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 = {
|
||||||
|
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…
x
Reference in New Issue
Block a user