From 85838dbb4c1af6e5ee319dc0b923236f23b101f4 Mon Sep 17 00:00:00 2001 From: Brandon Logan Date: Tue, 24 Mar 2015 18:21:41 -0500 Subject: [PATCH] Added neutron allowed address pairs network driver This driver will allocate a neutron port and fixed_ip on the VIP's network. Upon plugging the VIP, it will use neutron's allowed address pair to allow traffic destined for the ha_ip to pass to raised on amphora interfaces. Change-Id: I7bce4c2bbb9b35905c21caf79cb865e0ca146dac --- octavia/common/exceptions.py | 4 +- octavia/network/data_models.py | 3 +- .../drivers/neutron/allowed_address_pairs.py | 331 ++++++++++++++++++ octavia/tests/common/__init__.py | 0 octavia/tests/common/data_model_helpers.py | 76 ++++ .../unit/network/drivers/neutron/__init__.py | 0 .../neutron/test_allowed_address_pairs.py | 291 +++++++++++++++ 7 files changed, 703 insertions(+), 2 deletions(-) create mode 100644 octavia/network/drivers/neutron/allowed_address_pairs.py create mode 100644 octavia/tests/common/__init__.py create mode 100644 octavia/tests/common/data_model_helpers.py create mode 100644 octavia/tests/unit/network/drivers/neutron/__init__.py create mode 100644 octavia/tests/unit/network/drivers/neutron/test_allowed_address_pairs.py diff --git a/octavia/common/exceptions.py b/octavia/common/exceptions.py index fd233741ea..5a246a60c0 100644 --- a/octavia/common/exceptions.py +++ b/octavia/common/exceptions.py @@ -32,8 +32,10 @@ class OctaviaException(Exception): """ message = _("An unknown exception occurred.") - def __init__(self, **kwargs): + def __init__(self, *args, **kwargs): try: + if len(args) > 0: + self.message = args[0] super(OctaviaException, self).__init__(self.message % kwargs) self.msg = self.message % kwargs except Exception: diff --git a/octavia/network/data_models.py b/octavia/network/data_models.py index 618cf68333..9960646ec0 100644 --- a/octavia/network/data_models.py +++ b/octavia/network/data_models.py @@ -18,8 +18,9 @@ from octavia.common import data_models class Interface(data_models.BaseDataModel): def __init__(self, id=None, amphora_id=None, network_id=None, - ip_address=None): + ip_address=None, port_id=None): self.id = id self.amphora_id = amphora_id self.network_id = network_id + self.port_id = port_id self.ip_address = ip_address diff --git a/octavia/network/drivers/neutron/allowed_address_pairs.py b/octavia/network/drivers/neutron/allowed_address_pairs.py new file mode 100644 index 0000000000..e20389679b --- /dev/null +++ b/octavia/network/drivers/neutron/allowed_address_pairs.py @@ -0,0 +1,331 @@ +# Copyright 2014 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. + +from neutronclient.common import exceptions as neutron_client_exceptions +from neutronclient.neutron import client as neutron_client +from novaclient import client as nova_client +from novaclient import exceptions as nova_client_exceptions +from oslo_log import log as logging + +from octavia.common import data_models +from octavia.common import keystone +from octavia.i18n import _LE, _LI +from octavia.network import base +from octavia.network import data_models as network_models + + +LOG = logging.getLogger(__name__) +NEUTRON_VERSION = '2.0' +NOVA_VERSION = '2' +AAP_EXT_ALIAS = 'allowed-address-pairs' +SEC_GRP_EXT_ALIAS = 'security-group' +VIP_SECURITY_GRP_PREFIX = 'lb-' + + +class AllowedAddressPairsDriver(base.AbstractNetworkDriver): + + def __init__(self): + self.sec_grp_enabled = True + self.neutron_client = neutron_client.Client( + NEUTRON_VERSION, session=keystone.get_session()) + self._check_extensions_loaded() + self.nova_client = nova_client.Client( + NOVA_VERSION, session=keystone.get_session()) + + def _check_extensions_loaded(self): + extensions = self.neutron_client.list_extensions() + extensions = extensions.get('extensions') + aliases = [ext.get('alias') for ext in extensions] + if AAP_EXT_ALIAS not in aliases: + raise base.NetworkException( + 'The {alias} extension is not enabled in neutron. This ' + 'driver cannot be used with the {alias} extension ' + 'disabled.'.format(alias=AAP_EXT_ALIAS)) + if SEC_GRP_EXT_ALIAS not in aliases: + LOG.info(_LI('Neutron security groups are disabled. This driver' + 'will not manage any security groups.')) + self.sec_grp_enabled = False + + def _port_to_vip(self, port, load_balancer_id=None): + port = port['port'] + ip_address = port['fixed_ips'][0]['ip_address'] + network_id = port['network_id'] + port_id = port['id'] + return data_models.Vip(ip_address=ip_address, + network_id=network_id, + port_id=port_id, + load_balancer_id=load_balancer_id) + + def _nova_interface_to_octavia_interface(self, amphora_id, nova_interface): + ip_address = nova_interface.fixed_ips[0]['ip_address'] + return network_models.Interface(amphora_id=amphora_id, + network_id=nova_interface.net_id, + port_id=nova_interface.port_id, + ip_address=ip_address) + + def _get_interfaces_to_unplug(self, interfaces, network_id, + ip_address=None): + ret = [] + for interface_ in interfaces: + if interface_.net_id == network_id: + if ip_address: + for fixed_ip in interface_.fixed_ips: + if ip_address == fixed_ip.get('ip_address'): + ret.append(interface_) + else: + ret.append(interface_) + return ret + + def _get_plugged_interface(self, compute_id, network_id): + interfaces = self.get_plugged_networks(compute_id) + for interface in interfaces: + if interface.network_id == network_id: + return interface + + def _plug_amphora_vip(self, compute_id, network_id): + try: + interface = self.plug_network(compute_id, network_id) + except Exception: + message = _LE('Error plugging amphora (compute_id: {compute_id}) ' + 'into vip network {network_id}.').format( + compute_id=compute_id, + network_id=network_id) + LOG.exception(message) + raise base.PlugVIPException(message) + return interface + + def _add_vip_address_pair(self, port_id, vip_address): + try: + aap = { + 'port': { + 'allowed_address_pairs': [ + {'ip_address': vip_address} + ] + } + } + self.neutron_client.update_port(port_id, aap) + except neutron_client_exceptions.PortNotFoundClient as e: + raise base.PortNotFound(e.message) + except Exception: + message = _LE('Error adding allowed address pair {ip} ' + 'to port {port_id}.').format(ip=vip_address, + port_id=port_id) + LOG.exception(message) + raise base.PlugVIPException(message) + + def _get_lb_security_group(self, load_balancer_id): + sec_grp_name = VIP_SECURITY_GRP_PREFIX + load_balancer_id + sec_grps = self.neutron_client.list_security_groups(name=sec_grp_name) + if len(sec_grps.get('security_groups')): + return sec_grps.get('security_groups')[0] + + def _update_vip_security_group(self, load_balancer, vip): + sec_grp = self._get_lb_security_group(load_balancer.id) + if not sec_grp: + sec_grp_name = VIP_SECURITY_GRP_PREFIX + load_balancer.id + new_sec_grp = {'security_group': {'name': sec_grp_name}} + sec_grp = self.neutron_client.create_security_group(new_sec_grp) + sec_grp = sec_grp['security_group'] + for listener in load_balancer.listeners: + rule = { + 'security_group_rule': { + 'security_group_id': sec_grp.get('id'), + 'direction': 'ingress', + 'protocol': 'TCP', + 'port_range_min': listener.protocol_port, + 'port_range_max': listener.protocol_port + } + } + try: + self.neutron_client.create_security_group_rule(rule) + except neutron_client_exceptions.Conflict as conflict_e: + if 'already exists' not in conflict_e.message.lower(): + raise conflict_e + port_update = {'port': {'security_groups': [sec_grp.get('id')]}} + try: + self.neutron_client.update_port(vip.port_id, port_update) + except neutron_client_exceptions.PortNotFoundClient as e: + raise base.PortNotFound(e.message) + except Exception as e: + raise base.PlugVIPException(e.message) + + def _add_vip_security_group_to_amphorae(self, load_balancer_id, amphora): + sec_grp = self._get_lb_security_group(load_balancer_id) + self.nova_client.servers.add_security_group( + amphora.compute_id, sec_grp.get('id')) + + def deallocate_vip(self, vip): + try: + self.neutron_client.delete_port(vip.port_id) + except neutron_client_exceptions.PortNotFoundClient as e: + raise base.VIPConfigurationNotFound(e.message) + except Exception: + message = _LE('Error deleting VIP port_id {port_id} from ' + 'neutron').format(port_id=vip.port_id) + LOG.exception(message) + raise base.DeallocateVIPException(message) + + def plug_vip(self, load_balancer, vip): + if self.sec_grp_enabled: + self._update_vip_security_group(load_balancer, vip) + plugged_amphorae = [] + for amphora in load_balancer.amphorae: + interface = self._get_plugged_interface(amphora.compute_id, + vip.network_id) + if not interface: + interface = self._plug_amphora_vip(amphora.compute_id, + vip.network_id) + self._add_vip_address_pair(interface.port_id, vip.ip_address) + if self.sec_grp_enabled: + self._add_vip_security_group_to_amphorae( + load_balancer.id, amphora) + plugged_amphorae.append(data_models.Amphora( + id=amphora.id, + compute_id=amphora.compute_id, + vrrp_ip=interface.ip_address, + ha_ip=vip.ip_address)) + return plugged_amphorae + + def allocate_vip(self, port_id=None, network_id=None, ip_address=None): + if not port_id and not network_id: + raise base.AllocateVIPException('Cannot allocate a vip ' + 'without a port_id or ' + 'a network_id.') + if port_id: + LOG.info(_LI('Port {port_id} already exists. Nothing to be ' + 'done.').format(port_id=port_id)) + try: + port = self.neutron_client.show_port(port_id) + except neutron_client_exceptions.PortNotFoundClient as e: + raise base.PortNotFound(e.message) + except Exception: + message = _LE('Error retrieving info about port ' + '{port_id}.').format(port_id=port_id) + LOG.exception(message) + raise base.AllocateVIPException(message) + return self._port_to_vip(port) + + # It can be assumed that network_id exists + port = {'port': {'name': 'octavia-port', + 'network_id': network_id, + 'admin_state_up': False, + 'device_id': '', + 'device_owner': ''}} + try: + new_port = self.neutron_client.create_port(port) + except neutron_client_exceptions.NetworkNotFoundClient as e: + raise base.NetworkNotFound(e.message) + except Exception: + message = _LE('Error creating neutron port on network ' + '{network_id}.').format(network_id=network_id) + LOG.exception(message) + raise base.AllocateVIPException(message) + return self._port_to_vip(new_port) + + def unplug_vip(self, load_balancer, vip): + for amphora in load_balancer.amphorae: + interface = self._get_plugged_interface(amphora.compute_id, + vip.network_id) + if not interface: + # Thought about raising PluggedVIPNotFound exception but + # then that wouldn't evaluate all amphorae, so just continue + continue + try: + self.unplug_network(amphora.compute_id, vip.network_id) + except Exception: + pass + try: + aap_update = {'port': { + 'allowed_address_pairs': [] + }} + self.neutron_client.update_port(interface.port_id, + aap_update) + except Exception: + message = _LE('Error unplugging VIP. Could not clear ' + 'allowed address pairs from port ' + '{port_id}.').format(port_id=vip.port_id) + LOG.exception(message) + raise base.UnplugVIPException(message) + + def plug_network(self, compute_id, network_id, ip_address=None): + try: + interface_ = self.nova_client.servers.interface_attach( + server=compute_id, net_id=network_id, fixed_ip=ip_address, + port_id=None) + except nova_client_exceptions.NotFound as e: + if 'Instance' in e.message: + raise base.AmphoraNotFound(e.message) + if 'Network' in e.message: + raise base.NetworkNotFound(e.message) + except Exception: + message = _LE('Error plugging amphora (compute_id: {compute_id}) ' + 'into network {network_id}.').format( + compute_id=compute_id, + network_id=network_id) + LOG.exception(message) + raise base.PlugNetworkException(message) + + return self._nova_interface_to_octavia_interface(compute_id, + interface_) + + def get_plugged_networks(self, amphora_id): + try: + interfaces = self.nova_client.servers.interface_list( + server=amphora_id) + except Exception: + message = _LE('Error retrieving plugged networks for amphora ' + '{amphora_id}.').format(amphora_id=amphora_id) + LOG.exception(message) + raise base.NetworkException(message) + return [self._nova_interface_to_octavia_interface( + amphora_id, interface_) for interface_ in interfaces] + + def unplug_network(self, compute_id, network_id, ip_address=None): + try: + interfaces = self.nova_client.servers.interface_list( + server=compute_id) + except nova_client_exceptions.NotFound as e: + raise base.AmphoraNotFound(e.message) + except Exception: + message = _LE('Error retrieving nova interfaces for amphora ' + '(compute_id: {compute_id}) on network {network_id} ' + 'with ip {ip_address}.').format( + compute_id=compute_id, + network_id=network_id, + ip_address=ip_address) + LOG.exception(message) + raise base.NetworkException(message) + unpluggers = self._get_interfaces_to_unplug(interfaces, network_id, + ip_address=ip_address) + try: + for index, unplugger in enumerate(unpluggers): + self.nova_client.servers.interface_detach( + server=compute_id, port_id=unplugger.port_id) + except Exception: + message = _LE('Error unplugging amphora {amphora_id} from network ' + '{network_id}.').format(amphora_id=compute_id, + network_id=network_id) + if len(unpluggers) > 1: + message = _LE('{base} Other interfaces have been successfully ' + 'unplugged: ').format(base=message) + unpluggeds = unpluggers[:index] + for unplugged in unpluggeds: + message = _LE('{base} neutron port ' + '{port_id} ').format( + base=message, port_id=unplugged.port_id) + else: + message = _LE('{base} No other networks were ' + 'unplugged.').format(base=message) + LOG.exception(message) + raise base.UnplugNetworkException(message) diff --git a/octavia/tests/common/__init__.py b/octavia/tests/common/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/octavia/tests/common/data_model_helpers.py b/octavia/tests/common/data_model_helpers.py new file mode 100644 index 0000000000..48c7766672 --- /dev/null +++ b/octavia/tests/common/data_model_helpers.py @@ -0,0 +1,76 @@ +# Copyright 2014 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. + +from octavia.common import data_models + + +def generate_load_balancer_tree(): + vip = generate_vip() + amps = [generate_amphora(), generate_amphora()] + lb = generate_load_balancer(vip=vip, amphorae=amps) + return lb + + +LB_SEED = 0 + + +def generate_load_balancer(vip=None, amphorae=None): + amphorae = amphorae or [] + global LB_SEED + LB_SEED += 1 + lb = data_models.LoadBalancer(id='lb{0}-id'.format(LB_SEED), + tenant_id='2', + name='lb{0}'.format(LB_SEED), + description='lb{0}'.format(LB_SEED), + vip=vip, + amphorae=amphorae) + for amp in lb.amphorae: + amp.load_balancer = lb + amp.load_balancer_id = lb.id + if vip: + vip.load_balancer = lb + vip.load_balancer_id = lb.id + return lb + + +VIP_SEED = 0 + + +def generate_vip(load_balancer=None): + global VIP_SEED + VIP_SEED += 1 + vip = data_models.Vip(ip_address='10.0.0.{0}'.format(VIP_SEED), + network_id='net{0}-id'.format(VIP_SEED), + port_id='port{0}-id'.format(VIP_SEED), + load_balancer=load_balancer) + if load_balancer: + vip.load_balancer_id = load_balancer.id + return vip + + +AMP_SEED = 0 + + +def generate_amphora(load_balancer=None): + global AMP_SEED + AMP_SEED += 1 + amp = data_models.Amphora(id='amp{0}-id'.format(AMP_SEED), + compute_id='compute{0}-id'.format(AMP_SEED), + status='ACTIVE', + lb_network_ip='11.0.0.{0}'.format(AMP_SEED), + vrrp_ip='12.0.0.{0}'.format(AMP_SEED), + load_balancer=load_balancer) + if load_balancer: + amp.load_balancer_id = load_balancer.id + return amp \ No newline at end of file diff --git a/octavia/tests/unit/network/drivers/neutron/__init__.py b/octavia/tests/unit/network/drivers/neutron/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/octavia/tests/unit/network/drivers/neutron/test_allowed_address_pairs.py b/octavia/tests/unit/network/drivers/neutron/test_allowed_address_pairs.py new file mode 100644 index 0000000000..cc706c5534 --- /dev/null +++ b/octavia/tests/unit/network/drivers/neutron/test_allowed_address_pairs.py @@ -0,0 +1,291 @@ +# Copyright 2015 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. + +import mock +from neutronclient.common import exceptions as neutron_exceptions +from novaclient.client import exceptions as nova_exceptions + +from octavia.common import data_models +from octavia.network import base as network_base +from octavia.network.drivers.neutron import allowed_address_pairs +from octavia.tests.common import data_model_helpers as dmh +from octavia.tests.unit import base + + +class MockNovaInterface(object): + net_id = None + port_id = None + fixed_ips = [] + + +MOCK_NOVA_INTERFACE = MockNovaInterface() +MOCK_NOVA_INTERFACE.net_id = '1' +MOCK_NOVA_INTERFACE.port_id = '2' +MOCK_NOVA_INTERFACE.fixed_ips = [{'ip_address': '10.0.0.1'}] + +MOCK_NEUTRON_PORT = {'port': {'network_id': '1', + 'id': '2', + 'fixed_ips': [{'ip_address': '10.0.0.1'}]}} + + +class TestAllowedAddressPairsDriver(base.TestCase): + + def setUp(self): + super(TestAllowedAddressPairsDriver, self).setUp() + neutron_patcher = mock.patch('neutronclient.neutron.client.Client', + autospec=True) + mock.patch('novaclient.client.Client', autospec=True).start() + neutron_client = neutron_patcher.start() + client = neutron_client(allowed_address_pairs.NEUTRON_VERSION) + client.list_extensions.return_value = { + 'extensions': [{'alias': allowed_address_pairs.AAP_EXT_ALIAS}, + {'alias': allowed_address_pairs.SEC_GRP_EXT_ALIAS}]} + mock.patch('octavia.common.keystone.get_session').start() + self.driver = allowed_address_pairs.AllowedAddressPairsDriver() + + def test_check_extensions_loaded(self): + list_extensions = self.driver.neutron_client.list_extensions + list_extensions.return_value = { + 'extensions': [{'alias': 'blah'}]} + self.assertRaises(network_base.NetworkException, + self.driver._check_extensions_loaded) + + def test_port_to_vip(self): + fake_lb_id = '4' + vip = self.driver._port_to_vip(MOCK_NEUTRON_PORT, fake_lb_id) + self.assertIsInstance(vip, data_models.Vip) + self.assertEqual( + MOCK_NEUTRON_PORT['port']['fixed_ips'][0]['ip_address'], + vip.ip_address) + self.assertEqual(MOCK_NEUTRON_PORT['port']['network_id'], + vip.network_id) + self.assertEqual(MOCK_NEUTRON_PORT['port']['id'], vip.port_id) + self.assertEqual(fake_lb_id, vip.load_balancer_id) + + def test_nova_interface_to_octavia_interface(self): + nova_interface = MockNovaInterface() + nova_interface.net_id = '1' + nova_interface.port_id = '2' + nova_interface.fixed_ips = [{'ip_address': '10.0.0.1'}] + interface = self.driver._nova_interface_to_octavia_interface( + '3', nova_interface) + self.assertEqual('1', interface.network_id) + self.assertEqual('2', interface.port_id) + self.assertEqual('10.0.0.1', interface.ip_address) + + def test_get_interfaces_to_unplug(self): + if1 = MockNovaInterface() + if1.net_id = 'if1-net' + if1.port_id = 'if1-port' + if1.fixed_ips = [{'ip_address': '10.0.0.1'}] + if2 = MockNovaInterface() + if2.net_id = 'if2-net' + if2.port_id = 'if2-port' + if2.fixed_ips = [{'ip_address': '11.0.0.1'}] + interfaces = [if1, if2] + unpluggers = self.driver._get_interfaces_to_unplug( + interfaces, 'if1-net') + self.assertEqual([if1], unpluggers) + unpluggers = self.driver._get_interfaces_to_unplug( + interfaces, 'if1-net', ip_address='10.0.0.1') + self.assertEqual([if1], unpluggers) + unpluggers = self.driver._get_interfaces_to_unplug( + interfaces, 'if1-net', ip_address='11.0.0.1') + self.assertEqual([], unpluggers) + unpluggers = self.driver._get_interfaces_to_unplug( + interfaces, 'if3-net') + self.assertEqual([], unpluggers) + + def test_deallocate_vip(self): + vip = data_models.Vip(port_id='1') + self.driver.deallocate_vip(vip) + delete_port = self.driver.neutron_client.delete_port + delete_port.side_effect = neutron_exceptions.PortNotFoundClient + self.assertRaises(network_base.VIPConfigurationNotFound, + self.driver.deallocate_vip, vip) + delete_port.side_effect = TypeError + self.assertRaises(network_base.DeallocateVIPException, + self.driver.deallocate_vip, vip) + + def test_plug_vip(self): + interface_attach = self.driver.nova_client.servers.interface_attach + interface_attach.side_effect = nova_exceptions.NotFound + lb = dmh.generate_load_balancer_tree() + self.assertRaises(network_base.PlugVIPException, + self.driver.plug_vip, lb, lb.vip) + interface_attach.side_effect = None + interface_attach.return_value = MOCK_NOVA_INTERFACE + update_port = self.driver.neutron_client.update_port + update_port.side_effect = neutron_exceptions.PortNotFoundClient + self.assertRaises(network_base.PortNotFound, + self.driver.plug_vip, lb, lb.vip) + update_port.side_effect = TypeError + self.assertRaises(network_base.PlugVIPException, + self.driver.plug_vip, lb, lb.vip) + update_port.side_effect = None + update_port.reset_mock() + list_security_groups = self.driver.neutron_client.list_security_groups + list_security_groups.return_value = { + 'security_groups': [ + {'id': 'lb-sec-grp1'} + ] + } + mock_ip = MOCK_NOVA_INTERFACE.fixed_ips[0].get('ip_address') + expected_aap = {'port': {'allowed_address_pairs': + [{'ip_address': lb.vip.ip_address}]}} + interface_list = self.driver.nova_client.servers.interface_list + if1 = MOCK_NOVA_INTERFACE + if1.net_id = lb.vip.network_id + if2 = MockNovaInterface() + if2.net_id = '3' + if2.port_id = '4' + if2.fixed_ips = [{'ip_address': '10.0.0.2'}] + interface_list.return_value = [if1, if2] + amps = self.driver.plug_vip(lb, lb.vip) + self.assertEqual(3, update_port.call_count) + update_port.assert_any_call(if1.port_id, expected_aap) + for amp in amps: + self.assertEqual(mock_ip, amp.vrrp_ip) + self.assertEqual(lb.vip.ip_address, amp.ha_ip) + self.assertIn(amp.id, [lb.amphorae[0].id, lb.amphorae[1].id]) + + def test_allocate_vip(self): + self.assertRaises(network_base.AllocateVIPException, + self.driver.allocate_vip, port_id=None, + network_id=None) + show_port = self.driver.neutron_client.show_port + show_port.return_value = MOCK_NEUTRON_PORT + vip = self.driver.allocate_vip(port_id=MOCK_NEUTRON_PORT['port']['id']) + self.assertIsInstance(vip, data_models.Vip) + self.assertEqual( + MOCK_NEUTRON_PORT['port']['fixed_ips'][0]['ip_address'], + vip.ip_address) + self.assertEqual(MOCK_NEUTRON_PORT['port']['network_id'], + vip.network_id) + self.assertEqual(MOCK_NEUTRON_PORT['port']['id'], vip.port_id) + self.assertIsNone(vip.load_balancer_id) + + create_port = self.driver.neutron_client.create_port + create_port.return_value = MOCK_NEUTRON_PORT + vip = self.driver.allocate_vip( + network_id=MOCK_NEUTRON_PORT['port']['network_id']) + self.assertIsInstance(vip, data_models.Vip) + self.assertEqual( + MOCK_NEUTRON_PORT['port']['fixed_ips'][0]['ip_address'], + vip.ip_address) + self.assertEqual(MOCK_NEUTRON_PORT['port']['network_id'], + vip.network_id) + self.assertEqual(MOCK_NEUTRON_PORT['port']['id'], vip.port_id) + self.assertIsNone(vip.load_balancer_id) + + def test_unplug_vip(self): + lb = dmh.generate_load_balancer_tree() + interface_list = self.driver.nova_client.servers.interface_list + interface_list.reset_mock() + if1 = MOCK_NOVA_INTERFACE + if1.net_id = lb.vip.network_id + if2 = MockNovaInterface() + if2.net_id = '3' + if2.port_id = '4' + if2.fixed_ips = [{'ip_address': '10.0.0.2'}] + interface_list.return_value = [if1, if2] + update_port = self.driver.neutron_client.update_port + update_port.side_effect = neutron_exceptions.PortNotFoundClient + self.assertRaises(network_base.UnplugVIPException, + self.driver.unplug_vip, lb, lb.vip) + update_port.side_effect = TypeError + self.assertRaises(network_base.UnplugVIPException, + self.driver.unplug_vip, lb, lb.vip) + update_port.side_effect = None + update_port.reset_mock() + self.driver.unplug_vip(lb, lb.vip) + self.assertEqual(len(lb.amphorae), update_port.call_count) + clear_aap = {'port': {'allowed_address_pairs': []}} + update_port.assert_has_calls([mock.call(if1.port_id, clear_aap), + mock.call(if1.port_id, clear_aap)]) + + def test_plug_network(self): + amp_id = '1' + net_id = MOCK_NOVA_INTERFACE.net_id + interface_attach = self.driver.nova_client.servers.interface_attach + interface_attach.side_effect = nova_exceptions.NotFound( + 404, message='Instance not found') + self.assertRaises(network_base.AmphoraNotFound, + self.driver.plug_network, amp_id, net_id) + interface_attach.side_effect = nova_exceptions.NotFound( + 404, message='Network not found') + self.assertRaises(network_base.NetworkException, + self.driver.plug_network, amp_id, net_id) + interface_attach.side_effect = TypeError + self.assertRaises(network_base.PlugNetworkException, + self.driver.plug_network, amp_id, net_id) + interface_attach.side_effect = None + interface_attach.return_value = MOCK_NOVA_INTERFACE + oct_interface = self.driver.plug_network(amp_id, net_id) + self.assertEqual(MOCK_NOVA_INTERFACE.fixed_ips[0].get('ip_address'), + oct_interface.ip_address) + self.assertEqual(amp_id, oct_interface.amphora_id) + self.assertEqual(net_id, oct_interface.network_id) + + def test_get_plugged_networks(self): + amp_id = '1' + interface_list = self.driver.nova_client.servers.interface_list + interface_list.side_effect = TypeError + self.assertRaises(network_base.NetworkException, + self.driver.get_plugged_networks, amp_id) + interface_list.side_effect = None + interface_list.reset_mock() + if1 = MOCK_NOVA_INTERFACE + if2 = MockNovaInterface() + if2.net_id = '3' + if2.port_id = '4' + if2.fixed_ips = [{'ip_address': '10.0.0.2'}] + interface_list.return_value = [if1, if2] + plugged_networks = self.driver.get_plugged_networks(amp_id) + for pn in plugged_networks: + self.assertIn(pn.port_id, [if1.port_id, if2.port_id]) + self.assertIn(pn.network_id, [if1.net_id, if2.net_id]) + self.assertIn(pn.ip_address, [if1.fixed_ips[0]['ip_address'], + if2.fixed_ips[0]['ip_address']]) + + def test_unplug_network(self): + amp_id = '1' + net_id = MOCK_NOVA_INTERFACE.net_id + interface_list = self.driver.nova_client.servers.interface_list + interface_list.side_effect = nova_exceptions.NotFound(404) + self.assertRaises(network_base.AmphoraNotFound, + self.driver.unplug_network, amp_id, net_id) + interface_list.side_effect = Exception + self.assertRaises(network_base.NetworkException, + self.driver.unplug_network, amp_id, net_id) + interface_list.side_effect = None + interface_list.reset_mock() + if1 = MockNovaInterface() + if1.net_id = 'if1-net' + if1.port_id = 'if1-port' + if1.fixed_ips = [{'ip_address': '10.0.0.1'}] + if2 = MockNovaInterface() + if2.net_id = 'if2-net' + if2.port_id = 'if2-port' + if2.fixed_ips = [{'ip_address': '11.0.0.1'}] + interface_list.return_value = [if1, if2] + interface_detach = self.driver.nova_client.servers.interface_detach + interface_detach.side_effect = Exception + self.assertRaises(network_base.UnplugNetworkException, + self.driver.unplug_network, amp_id, if2.net_id) + interface_detach.side_effect = None + interface_detach.reset_mock() + self.driver.unplug_network(amp_id, if2.net_id) + interface_detach.assert_called_once_with(server=amp_id, + port_id=if2.port_id)