diff --git a/hyperv/neutron/hyperv_neutron_agent.py b/hyperv/neutron/hyperv_neutron_agent.py index 9d87904..9db4ce1 100644 --- a/hyperv/neutron/hyperv_neutron_agent.py +++ b/hyperv/neutron/hyperv_neutron_agent.py @@ -250,12 +250,7 @@ networking-plugin-hyperv_agent.html self._utils.connect_vnic_to_vswitch(map['vswitch_name'], port_id) if network_type == constants.TYPE_VLAN: - LOG.info(_LI('Binding VLAN ID %(segmentation_id)s ' - 'to switch port %(port_id)s'), - dict(segmentation_id=segmentation_id, port_id=port_id)) - self._utils.set_vswitch_port_vlan_id( - segmentation_id, - port_id) + self._vlan_driver.bind_vlan_port(port_id, segmentation_id) elif network_type == constants.TYPE_NVGRE and self._nvgre_enabled: self._nvgre_ops.bind_nvgre_port( segmentation_id, map['vswitch_name'], port_id) diff --git a/hyperv/neutron/l2_agent.py b/hyperv/neutron/l2_agent.py index 2b07047..3337d7f 100755 --- a/hyperv/neutron/l2_agent.py +++ b/hyperv/neutron/l2_agent.py @@ -34,6 +34,7 @@ from oslo_service import loopingcall from hyperv.common.i18n import _, _LE, _LI # noqa from hyperv.neutron import constants as h_const from hyperv.neutron import hyperv_neutron_agent +from hyperv.neutron import trunk_driver LOG = logging.getLogger(__name__) CONF = cfg.CONF @@ -151,6 +152,8 @@ class HyperVNeutronAgent(hyperv_neutron_agent.HyperVNeutronAgentMixin): self._report_state) heartbeat.start(interval=report_interval) + self._vlan_driver = trunk_driver.HyperVTrunkDriver(self.context) + def main(): config.register_agent_state_opts_helper(cfg.CONF) diff --git a/hyperv/neutron/trunk_driver.py b/hyperv/neutron/trunk_driver.py new file mode 100644 index 0000000..d0e3bda --- /dev/null +++ b/hyperv/neutron/trunk_driver.py @@ -0,0 +1,147 @@ +# Copyright 2017 Cloudbase Solutions Srl +# All Rights Reserved. +# +# 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 neutron.api.rpc.callbacks import events +from neutron.api.rpc.handlers import resources_rpc +from neutron.services.trunk import constants as t_const +from neutron.services.trunk.rpc import agent as trunk_rpc +from os_win import constants as os_win_const +from os_win import utilsfactory +from oslo_log import log as logging +import oslo_messaging + +from hyperv.common.i18n import _LI, _LE # noqa + +LOG = logging.getLogger(__name__) + + +class HyperVTrunkDriver(trunk_rpc.TrunkSkeleton): + """Driver responsible for handling trunk/subport/port events. + + Receives data model events from the neutron server and uses them to setup + VLAN trunks for Hyper-V vSwitch ports. + """ + + def __init__(self, context): + super(HyperVTrunkDriver, self).__init__() + self._context = context + self._utils = utilsfactory.get_networkutils() + self._trunk_rpc = trunk_rpc.TrunkStub() + + # Map between trunk.id and trunk. + self._trunks = {} + + def handle_trunks(self, trunks, event_type): + """Trunk data model change from the server.""" + + LOG.debug("Trunks event received: %(event_type)s. Trunks: %(trunks)s", + {'event_type': event_type, 'trunks': trunks}) + + if event_type == events.DELETED: + # The port trunks have been deleted. Remove them from cache. + for trunk in trunks: + self._trunks.pop(trunk.id, None) + else: + for trunk in trunks: + self._trunks[trunk.id] = trunk + self._setup_trunk(trunk) + + def handle_subports(self, subports, event_type): + """Subport data model change from the server.""" + + LOG.debug("Subports event received: %(event_type)s. " + "Subports: %(subports)s", + {'event_type': event_type, 'subports': subports}) + + # update the cache. + if event_type == events.CREATED: + for subport in subports: + trunk = self._trunks.get(subport['trunk_id']) + if trunk: + trunk.sub_ports.append(subport) + elif event_type == events.DELETED: + for subport in subports: + trunk = self._trunks.get(subport['trunk_id']) + if trunk and subport in trunk.sub_ports: + trunk.sub_ports.remove(subport) + + # update the bound trunks. + affected_trunk_ids = set([s['trunk_id'] for s in subports]) + for trunk_id in affected_trunk_ids: + trunk = self._trunks.get(trunk_id) + if trunk: + self._setup_trunk(trunk) + + def bind_vlan_port(self, port_id, segmentation_id): + trunk = self._fetch_trunk(port_id) + if not trunk: + # No trunk found. No VLAN IDs to set in trunk mode. + self._set_port_vlan(port_id, segmentation_id) + return + + self._setup_trunk(trunk, segmentation_id) + + def _fetch_trunk(self, port_id, context=None): + context = context or self._context + try: + trunk = self._trunk_rpc.get_trunk_details(context, port_id) + LOG.debug("Found trunk for port_id %(port_id)s: %(trunk)s", + {'port_id': port_id, 'trunk': trunk}) + + # cache it. + self._trunks[trunk.id] = trunk + return trunk + except resources_rpc.ResourceNotFound: + return None + except oslo_messaging.RemoteError as ex: + if 'CallbackNotFound' not in str(ex): + raise + LOG.debug("Trunk plugin disabled on server. Assuming port %s is " + "not a trunk.", port_id) + return None + + def _setup_trunk(self, trunk, vlan_id=None): + """Sets up VLAN trunk and updates the trunk status.""" + + LOG.info(_LI('Binding trunk port: %s.'), trunk) + try: + # bind sub_ports to host. + self._trunk_rpc.update_subport_bindings(self._context, + trunk.sub_ports) + + vlan_trunk = [s.segmentation_id for s in trunk.sub_ports] + self._set_port_vlan(trunk.port_id, vlan_id, vlan_trunk) + + self._trunk_rpc.update_trunk_status(self._context, trunk.id, + t_const.ACTIVE_STATUS) + except Exception: + # something broke + LOG.exception(_LE("Failure setting up subports for %s"), + trunk.port_id) + self._trunk_rpc.update_trunk_status(self._context, trunk.id, + t_const.DEGRADED_STATUS) + + def _set_port_vlan(self, port_id, vlan_id, vlan_trunk=None): + LOG.info(_LI('Binding VLAN ID: %(vlan_id)s, VLAN trunk: ' + '%(vlan_trunk)s to switch port %(port_id)s'), + dict(vlan_id=vlan_id, vlan_trunk=vlan_trunk, port_id=port_id)) + + op_mode = (os_win_const.VLAN_MODE_TRUNK if vlan_trunk else + os_win_const.VLAN_MODE_ACCESS) + self._utils.set_vswitch_port_vlan_id( + vlan_id, + port_id, + operation_mode=op_mode, + vlan_trunk=vlan_trunk) diff --git a/hyperv/tests/base.py b/hyperv/tests/base.py index 949a4ae..889861f 100644 --- a/hyperv/tests/base.py +++ b/hyperv/tests/base.py @@ -25,6 +25,7 @@ import traceback import eventlet.timeout import fixtures import mock +from os_win import utilsfactory from oslo_config import cfg from oslo_utils import strutils import testtools @@ -136,3 +137,12 @@ class BaseTestCase(testtools.TestCase): group = kw.pop('group', None) for k, v in kw.items(): CONF.set_override(k, v, group) + + +class HyperVBaseTestCase(BaseTestCase): + def setUp(self): + super(HyperVBaseTestCase, self).setUp() + + utilsfactory_patcher = mock.patch.object(utilsfactory, '_get_class') + utilsfactory_patcher.start() + self.addCleanup(utilsfactory_patcher.stop) diff --git a/hyperv/tests/unit/neutron/test_hyperv_neutron_agent.py b/hyperv/tests/unit/neutron/test_hyperv_neutron_agent.py index 5e685cc..0f9f5e5 100644 --- a/hyperv/tests/unit/neutron/test_hyperv_neutron_agent.py +++ b/hyperv/tests/unit/neutron/test_hyperv_neutron_agent.py @@ -20,7 +20,6 @@ Unit tests for Windows Hyper-V virtual switch neutron driver import mock from os_win import exceptions -from os_win import utilsfactory from oslo_config import cfg from hyperv.neutron import constants @@ -31,15 +30,12 @@ from hyperv.tests import base CONF = cfg.CONF -class TestHyperVNeutronAgent(base.BaseTestCase): +class TestHyperVNeutronAgent(base.HyperVBaseTestCase): _FAKE_PORT_ID = 'fake_port_id' def setUp(self): super(TestHyperVNeutronAgent, self).setUp() - utilsfactory_patcher = mock.patch.object(utilsfactory, '_get_class') - utilsfactory_patcher.start() - self.addCleanup(utilsfactory_patcher.stop) self.agent = hyperv_neutron_agent.HyperVNeutronAgentMixin() self.agent._qos_ext = mock.MagicMock() @@ -54,6 +50,7 @@ class TestHyperVNeutronAgent(base.BaseTestCase): self.agent.notifier = mock.Mock() self.agent._utils = mock.MagicMock() self.agent._nvgre_ops = mock.MagicMock() + self.agent._vlan_driver = mock.MagicMock() def test_load_physical_network_mappings(self): test_mappings = ['fakenetwork1:fake_vswitch', @@ -338,9 +335,7 @@ class TestHyperVNeutronAgent(base.BaseTestCase): @mock.patch.object(hyperv_neutron_agent.HyperVNeutronAgentMixin, '_provision_network') - def test_port_bound_nvgre(self, mock_provision_network): - self.agent._nvgre_enabled = True - network_type = constants.TYPE_NVGRE + def _check_port_bound_net_type(self, mock_provision_network, network_type): net_uuid = 'my-net-uuid' fake_map = {'vswitch_name': mock.sentinel.vswitch_name, 'ports': []} @@ -360,6 +355,17 @@ class TestHyperVNeutronAgent(base.BaseTestCase): mock.sentinel.physical_network, mock.sentinel.segmentation_id) self.agent._utils.connect_vnic_to_vswitch.assert_called_once_with( mock.sentinel.vswitch_name, mock.sentinel.port_id) + + def test_port_bound_vlan(self): + self._check_port_bound_net_type(network_type=constants.TYPE_VLAN) + + self.agent._vlan_driver.bind_vlan_port.assert_called_once_with( + mock.sentinel.port_id, mock.sentinel.segmentation_id) + + def test_port_bound_nvgre(self): + self.agent._nvgre_enabled = True + self._check_port_bound_net_type(network_type=constants.TYPE_NVGRE) + self.agent._nvgre_ops.bind_nvgre_port.assert_called_once_with( mock.sentinel.segmentation_id, mock.sentinel.vswitch_name, mock.sentinel.port_id) diff --git a/hyperv/tests/unit/neutron/test_l2_agent.py b/hyperv/tests/unit/neutron/test_l2_agent.py index de3e0c4..b43387d 100644 --- a/hyperv/tests/unit/neutron/test_l2_agent.py +++ b/hyperv/tests/unit/neutron/test_l2_agent.py @@ -119,6 +119,7 @@ class TestHyperVNeutronAgent(base.BaseTestCase): self.agent.context, {'start_flag': True}) self.assertTrue(self.agent.agent_state['start_flag']) + @mock.patch.object(l2_agent.trunk_driver, 'HyperVTrunkDriver') @mock.patch.object(l2_agent.qos_extension, 'QosAgentExtension') @mock.patch.object(l2_agent.loopingcall, 'FixedIntervalLoopingCall') @mock.patch.object(l2_agent.n_rpc, 'get_client') @@ -128,7 +129,7 @@ class TestHyperVNeutronAgent(base.BaseTestCase): @mock.patch.object(l2_agent, 'CONF') def test_setup_rpc(self, mock_CONF, mock_agent_rpc, mock_SGRpcApi, mock_HyperVSecurityAgent, mock_get_client, - mock_LoopingCall, mock_qos_ext): + mock_LoopingCall, mock_qos_ext, mock_HyperVTrunkDriver): mock_CONF.NVGRE.enable_support = True mock_CONF.AGENT.report_interval = mock.sentinel.report_interval mock_CONF.AGENT.enable_qos_extension = True @@ -144,6 +145,8 @@ class TestHyperVNeutronAgent(base.BaseTestCase): self.assertEqual(mock_agent_rpc.create_consumers.return_value, self.agent.connection) self.assertEqual(mock_get_client.return_value, self.agent.client) + self.assertEqual(mock_HyperVTrunkDriver.return_value, + self.agent._vlan_driver) mock_HyperVSecurityAgent.assert_called_once_with( self.agent.context, self.agent.sg_plugin_rpc) @@ -163,6 +166,7 @@ class TestHyperVNeutronAgent(base.BaseTestCase): self.agent.connection.consume_in_threads.assert_called_once_with() mock_LoopingCall.return_value.start.assert_called_once_with( interval=mock.sentinel.report_interval) + mock_HyperVTrunkDriver.assert_called_once_with(self.agent.context) class TestMain(base.BaseTestCase): diff --git a/hyperv/tests/unit/neutron/test_nvgre_ops.py b/hyperv/tests/unit/neutron/test_nvgre_ops.py index bfd2854..3a6b81e 100644 --- a/hyperv/tests/unit/neutron/test_nvgre_ops.py +++ b/hyperv/tests/unit/neutron/test_nvgre_ops.py @@ -18,7 +18,6 @@ Unit tests for Windows Hyper-V NVGRE driver. """ import mock -from os_win import utilsfactory from oslo_config import cfg from hyperv.neutron import constants @@ -28,7 +27,7 @@ from hyperv.tests import base CONF = cfg.CONF -class TestHyperVNvgreOps(base.BaseTestCase): +class TestHyperVNvgreOps(base.HyperVBaseTestCase): FAKE_MAC_ADDR = 'fa:ke:ma:ca:dd:re:ss' FAKE_CIDR = '10.0.0.0/24' @@ -36,9 +35,6 @@ class TestHyperVNvgreOps(base.BaseTestCase): def setUp(self): super(TestHyperVNvgreOps, self).setUp() - utilsfactory_patcher = mock.patch.object(utilsfactory, '_get_class') - utilsfactory_patcher.start() - self.addCleanup(utilsfactory_patcher.stop) self.context = 'context' self.ops = nvgre_ops.HyperVNvgreOps([]) diff --git a/hyperv/tests/unit/neutron/test_security_groups_driver.py b/hyperv/tests/unit/neutron/test_security_groups_driver.py index f968de0..d91af5e 100644 --- a/hyperv/tests/unit/neutron/test_security_groups_driver.py +++ b/hyperv/tests/unit/neutron/test_security_groups_driver.py @@ -19,13 +19,12 @@ Unit tests for the Hyper-V Security Groups Driver. import mock from os_win import exceptions -from os_win import utilsfactory from hyperv.neutron import security_groups_driver as sg_driver from hyperv.tests import base -class SecurityGroupRuleTestHelper(base.BaseTestCase): +class SecurityGroupRuleTestHelper(base.HyperVBaseTestCase): _FAKE_DIRECTION = 'egress' _FAKE_ETHERTYPE = 'IPv4' _FAKE_ETHERTYPE_IPV6 = 'IPv6' @@ -68,9 +67,6 @@ class TestHyperVSecurityGroupsDriver(SecurityGroupRuleTestHelper): def setUp(self): super(TestHyperVSecurityGroupsDriver, self).setUp() - utilsfactory_patcher = mock.patch.object(utilsfactory, '_get_class') - utilsfactory_patcher.start() - self.addCleanup(utilsfactory_patcher.stop) self._driver = sg_driver.HyperVSecurityGroupsDriver() self._driver._utils = mock.MagicMock() diff --git a/hyperv/tests/unit/neutron/test_trunk_driver.py b/hyperv/tests/unit/neutron/test_trunk_driver.py new file mode 100644 index 0000000..746b85c --- /dev/null +++ b/hyperv/tests/unit/neutron/test_trunk_driver.py @@ -0,0 +1,153 @@ +# Copyright 2017 Cloudbase Solutions Srl +# All Rights Reserved. +# +# 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. + +""" +Unit tests for the Hyper-V Trunk Driver. +""" + +import mock +from neutron.api.rpc.callbacks import events +from neutron.api.rpc.handlers import resources_rpc +from neutron.services.trunk import constants as t_const +from os_win import constants as os_win_const +import oslo_messaging +import testtools + +from hyperv.neutron import trunk_driver +from hyperv.tests import base + + +class TestHyperVTrunkDriver(base.HyperVBaseTestCase): + + @mock.patch.object(trunk_driver.trunk_rpc, 'TrunkStub', + lambda *args, **kwargs: None) + @mock.patch.object(trunk_driver.trunk_rpc.TrunkSkeleton, '__init__', + lambda *args, **kwargs: None) + def setUp(self): + super(TestHyperVTrunkDriver, self).setUp() + + self.trunk_driver = trunk_driver.HyperVTrunkDriver( + mock.sentinel.context) + self.trunk_driver._utils = mock.MagicMock() + self.trunk_driver._trunk_rpc = mock.MagicMock() + + def test_handle_trunks_deleted(self): + mock_trunk = mock.MagicMock() + self.trunk_driver._trunks[mock_trunk.id] = mock_trunk + + self.trunk_driver.handle_trunks([mock_trunk], events.DELETED) + self.assertNotIn(mock_trunk.id, self.trunk_driver._trunks) + + @mock.patch.object(trunk_driver.HyperVTrunkDriver, '_setup_trunk') + def test_handle_trunks_created(self, mock_setup_trunk): + sub_ports = [] + mock_trunk = mock.MagicMock(sub_ports=sub_ports) + + self.trunk_driver.handle_trunks([mock_trunk], events.CREATED) + + self.assertEqual(mock_trunk, self.trunk_driver._trunks[mock_trunk.id]) + mock_setup_trunk.assert_called_once_with(mock_trunk) + + @mock.patch.object(trunk_driver.HyperVTrunkDriver, '_set_port_vlan') + @mock.patch.object(trunk_driver.HyperVTrunkDriver, '_fetch_trunk') + def test_bind_vlan_port_not_trunk(self, mock_fetch_trunk, mock_set_vlan): + mock_fetch_trunk.return_value = None + + self.trunk_driver.bind_vlan_port(mock.sentinel.port_id, + mock.sentinel.segmentation_id) + + mock_fetch_trunk.assert_called_once_with(mock.sentinel.port_id) + mock_set_vlan.assert_called_once_with(mock.sentinel.port_id, + mock.sentinel.segmentation_id) + + @mock.patch.object(trunk_driver.HyperVTrunkDriver, '_setup_trunk') + @mock.patch.object(trunk_driver.HyperVTrunkDriver, '_fetch_trunk') + def test_bind_vlan_port(self, mock_fetch_trunk, mock_setup_trunk): + self.trunk_driver.bind_vlan_port(mock.sentinel.port_id, + mock.sentinel.segmentation_id) + + mock_fetch_trunk.assert_called_once_with(mock.sentinel.port_id) + mock_setup_trunk.assert_called_once_with(mock_fetch_trunk.return_value, + mock.sentinel.segmentation_id) + + def test_fetch_trunk(self): + mock_trunk = ( + self.trunk_driver._trunk_rpc.get_trunk_details.return_value) + + trunk = self.trunk_driver._fetch_trunk(mock.sentinel.port_id, + mock.sentinel.context) + + self.assertEqual(mock_trunk, trunk) + self.assertEqual(mock_trunk, self.trunk_driver._trunks[mock_trunk.id]) + self.trunk_driver._trunk_rpc.get_trunk_details.assert_called_once_with( + mock.sentinel.context, mock.sentinel.port_id) + + def test_fetch_trunk_resource_not_found(self): + self.trunk_driver._trunk_rpc.get_trunk_details.side_effect = ( + resources_rpc.ResourceNotFound) + + trunk = self.trunk_driver._fetch_trunk(mock.sentinel.port_id) + self.assertIsNone(trunk) + + def test_fetch_trunk_resource_remote_error(self): + self.trunk_driver._trunk_rpc.get_trunk_details.side_effect = ( + oslo_messaging.RemoteError('expected CallbackNotFound')) + + trunk = self.trunk_driver._fetch_trunk(mock.sentinel.port_id) + self.assertIsNone(trunk) + + def test_fetch_trunk_resource_remote_error_reraised(self): + self.trunk_driver._trunk_rpc.get_trunk_details.side_effect = ( + oslo_messaging.RemoteError) + + self.assertRaises(oslo_messaging.RemoteError, + self.trunk_driver._fetch_trunk, + mock.sentinel.port_id) + + @mock.patch.object(trunk_driver.HyperVTrunkDriver, '_set_port_vlan') + def test_setup_trunk(self, mock_set_vlan): + mock_subport = mock.MagicMock() + mock_trunk = mock.MagicMock(sub_ports=[mock_subport]) + trunk_rpc = self.trunk_driver._trunk_rpc + trunk_rpc.update_trunk_status.side_effect = [ + testtools.ExpectedException, None] + + self.trunk_driver._setup_trunk(mock_trunk, mock.sentinel.vlan_id) + + trunk_rpc.update_subport_bindings.assert_called_once_with( + self.trunk_driver._context, [mock_subport]) + mock_set_vlan.assert_called_once_with( + mock_trunk.port_id, mock.sentinel.vlan_id, + [mock_subport.segmentation_id]) + mock_set_vlan.has_calls([ + mock.call(self.trunk_driver._context, mock_trunk.id, status) + for status in [t_const.ACTIVE_STATUS, t_const.DEGRADED_STATUS]]) + + def _check_set_port_vlan(self, vlan_trunk, operation_mode): + self.trunk_driver._set_port_vlan(mock.sentinel.port_id, + mock.sentinel.vlan_id, + vlan_trunk) + + self.trunk_driver._utils.set_vswitch_port_vlan_id( + mock.sentinel.vlan_id, mock.sentinel.port_id, + operation_mode=operation_mode, + vlan_trunk=vlan_trunk) + + def test_set_port_vlan_trunk_mode(self): + self._check_set_port_vlan(mock.sentinel.vlan_trunk, + os_win_const.VLAN_MODE_TRUNK) + + def test_set_port_vlan_access_mode(self): + self._check_set_port_vlan(None, os_win_const.VLAN_MODE_ACCESS)