From eb9958fe7fd57d68f8c9d5b871003be221fe56e4 Mon Sep 17 00:00:00 2001 From: Brandon Logan Date: Thu, 9 Jun 2016 01:09:27 -0500 Subject: [PATCH] DHCP Agent scheduling with segments Since a network can now be broken up by segments, each segment will need to have its own DHCP Agent. This maintains backwards compatibility when a network does not have a segment. However, once a segment is created on a network, a dhcp agent should be scheduled per segment with a dhcp enabled subnet. The scheduling happens by filtering the candidate dhcp agents by the hosts that are bound to that segment. Partially-Implements: blueprint routed-networks Change-Id: If73211978e14b7533a1213cfb8c2c155a408f19e --- .../rpc/agentnotifiers/dhcp_rpc_agent_api.py | 12 ++- neutron/db/agentschedulers_db.py | 5 +- neutron/scheduler/dhcp_agent_scheduler.py | 21 ++-- neutron/services/segments/db.py | 4 +- .../agentnotifiers/test_dhcp_rpc_agent_api.py | 27 +++++- neutron/tests/unit/extensions/test_segment.py | 92 ++++++++++++++++++ .../scheduler/test_dhcp_agent_scheduler.py | 95 +++++++++++++++++++ 7 files changed, 243 insertions(+), 13 deletions(-) diff --git a/neutron/api/rpc/agentnotifiers/dhcp_rpc_agent_api.py b/neutron/api/rpc/agentnotifiers/dhcp_rpc_agent_api.py index 795b522afd0..e59343ef305 100644 --- a/neutron/api/rpc/agentnotifiers/dhcp_rpc_agent_api.py +++ b/neutron/api/rpc/agentnotifiers/dhcp_rpc_agent_api.py @@ -137,9 +137,17 @@ class DhcpAgentNotifyAPI(object): elif cast_required: admin_ctx = (context if context.is_admin else context.elevated()) network = self.plugin.get_network(admin_ctx, network_id) - agents = self.plugin.get_dhcp_agents_hosting_networks( - context, [network_id]) + if 'subnet' in payload and payload['subnet'].get('segment_id'): + # if segment_id exists then the segment service plugin + # must be loaded + nm = manager.NeutronManager + segment_plugin = nm.get_service_plugins()['segments'] + segment = segment_plugin.get_segment( + context, payload['subnet']['segment_id']) + network['candidate_hosts'] = segment['hosts'] + agents = self.plugin.get_dhcp_agents_hosting_networks( + context, [network_id], hosts=network.get('candidate_hosts')) # schedule the network first, if needed schedule_required = ( method == 'subnet_create_end' or diff --git a/neutron/db/agentschedulers_db.py b/neutron/db/agentschedulers_db.py index 9111dff4ea1..e0285d93db1 100644 --- a/neutron/db/agentschedulers_db.py +++ b/neutron/db/agentschedulers_db.py @@ -438,7 +438,8 @@ class DhcpAgentSchedulerDbMixin(dhcpagentscheduler "rescheduling")) def get_dhcp_agents_hosting_networks( - self, context, network_ids, active=None, admin_state_up=None): + self, context, network_ids, active=None, admin_state_up=None, + hosts=None): if not network_ids: return [] query = context.session.query(ndab_model.NetworkDhcpAgentBinding) @@ -448,6 +449,8 @@ class DhcpAgentSchedulerDbMixin(dhcpagentscheduler if network_ids: query = query.filter( ndab_model.NetworkDhcpAgentBinding.network_id.in_(network_ids)) + if hosts: + query = query.filter(agents_db.Agent.host.in_(hosts)) if admin_state_up is not None: query = query.filter(agents_db.Agent.admin_state_up == admin_state_up) diff --git a/neutron/scheduler/dhcp_agent_scheduler.py b/neutron/scheduler/dhcp_agent_scheduler.py index 1a106a91713..7b3d66002b9 100644 --- a/neutron/scheduler/dhcp_agent_scheduler.py +++ b/neutron/scheduler/dhcp_agent_scheduler.py @@ -199,6 +199,18 @@ class DhcpFilter(base_resource_filter.BaseResourceFilter): 'hosted_agents': agents_dict['hosted_agents']} return agents_dict + def _filter_agents_with_network_access(self, hostable_agents, plugin, + context, network): + if 'candidate_hosts' in network: + hostable_dhcp_hosts = network['candidate_hosts'] + else: + hostable_dhcp_hosts = plugin.filter_hosts_with_network_access( + context, network['id'], + [agent['host'] for agent in hostable_agents]) + reachable_agents = [agent for agent in hostable_agents + if agent['host'] in hostable_dhcp_hosts] + return reachable_agents + def _get_dhcp_agents_hosting_network(self, plugin, context, network): """Return dhcp agents hosting the given network or None if a given network is already hosted by enough number of agents. @@ -208,7 +220,7 @@ class DhcpFilter(base_resource_filter.BaseResourceFilter): # subnets whose enable_dhcp is false with context.session.begin(subtransactions=True): network_hosted_agents = plugin.get_dhcp_agents_hosting_networks( - context, [network['id']]) + context, [network['id']], hosts=network.get('candidate_hosts')) if len(network_hosted_agents) >= agents_per_network: LOG.debug('Network %s is already hosted by enough agents.', network['id']) @@ -253,11 +265,8 @@ class DhcpFilter(base_resource_filter.BaseResourceFilter): context, True, agent) ] - hostable_dhcp_hosts = plugin.filter_hosts_with_network_access( - context, network['id'], - [agent['host'] for agent in hostable_dhcp_agents]) - hostable_dhcp_agents = [agent for agent in hostable_dhcp_agents - if agent['host'] in hostable_dhcp_hosts] + hostable_dhcp_agents = self._filter_agents_with_network_access( + hostable_dhcp_agents, plugin, context, network) if not hostable_dhcp_agents: return {'n_agents': 0, 'hostable_agents': [], diff --git a/neutron/services/segments/db.py b/neutron/services/segments/db.py index 84a65ffc5f3..a6dae924487 100644 --- a/neutron/services/segments/db.py +++ b/neutron/services/segments/db.py @@ -77,7 +77,9 @@ class SegmentDbMixin(common_db_mixin.CommonDbMixin): 'network_id': segment_db['network_id'], db.PHYSICAL_NETWORK: segment_db[db.PHYSICAL_NETWORK], db.NETWORK_TYPE: segment_db[db.NETWORK_TYPE], - db.SEGMENTATION_ID: segment_db[db.SEGMENTATION_ID]} + db.SEGMENTATION_ID: segment_db[db.SEGMENTATION_ID], + 'hosts': [mapping.host for mapping in + segment_db.segment_host_mapping]} return self._fields(res, fields) def _get_segment(self, context, segment_id): diff --git a/neutron/tests/unit/api/rpc/agentnotifiers/test_dhcp_rpc_agent_api.py b/neutron/tests/unit/api/rpc/agentnotifiers/test_dhcp_rpc_agent_api.py index 511c2748482..fd2792687be 100644 --- a/neutron/tests/unit/api/rpc/agentnotifiers/test_dhcp_rpc_agent_api.py +++ b/neutron/tests/unit/api/rpc/agentnotifiers/test_dhcp_rpc_agent_api.py @@ -151,10 +151,12 @@ class TestDhcpAgentNotifyAPI(base.BaseTestCase): self.assertEqual(expected_casts, self.mock_cast.call_count) def _test__notify_agents(self, method, - expected_scheduling=0, expected_casts=0): + expected_scheduling=0, expected_casts=0, + payload=None): + payload = payload or {'port': {}} self._test__notify_agents_with_function( lambda: self.notifier._notify_agents( - mock.Mock(), method, {'port': {}}, 'foo_network_id'), + mock.Mock(), method, payload, 'foo_network_id'), expected_scheduling, expected_casts) def test__notify_agents_cast_required_with_scheduling(self): @@ -167,7 +169,26 @@ class TestDhcpAgentNotifyAPI(base.BaseTestCase): def test__notify_agents_cast_required_with_scheduling_subnet_create(self): self._test__notify_agents('subnet_create_end', - expected_scheduling=1, expected_casts=1) + expected_scheduling=1, expected_casts=1, + payload={'subnet': {}}) + + def test__notify_agents_cast_required_with_scheduling_segment(self): + network_id = 'foo_network_id' + segment_id = 'foo_segment_id' + subnet = {'subnet': {'segment_id': segment_id}} + segment = {'id': segment_id, 'network_id': network_id, + 'hosts': ['host-a']} + self.notifier.plugin.get_network.return_value = {'id': network_id} + segment_sp = mock.Mock() + segment_sp.get_segment.return_value = segment + with mock.patch('neutron.manager.NeutronManager.get_service_plugins', + return_value={'segments': segment_sp}): + self._test__notify_agents('subnet_create_end', + expected_scheduling=1, expected_casts=1, + payload=subnet) + get_agents = self.notifier.plugin.get_dhcp_agents_hosting_networks + get_agents.assert_called_once_with( + mock.ANY, [network_id], hosts=segment['hosts']) def test__notify_agents_no_action(self): self._test__notify_agents('network_create_end', diff --git a/neutron/tests/unit/extensions/test_segment.py b/neutron/tests/unit/extensions/test_segment.py index 42269c698a7..a150a89cb1f 100644 --- a/neutron/tests/unit/extensions/test_segment.py +++ b/neutron/tests/unit/extensions/test_segment.py @@ -21,6 +21,7 @@ import webob.exc from neutron.api.v2 import attributes from neutron import context from neutron.db import agents_db +from neutron.db import agentschedulers_db from neutron.db import db_base_plugin_v2 from neutron.db import portbindings_db from neutron.db import segments_db @@ -36,6 +37,8 @@ from neutron.tests.unit.db import test_db_base_plugin_v2 SERVICE_PLUGIN_KLASS = 'neutron.services.segments.plugin.Plugin' TEST_PLUGIN_KLASS = ( 'neutron.tests.unit.extensions.test_segment.SegmentTestPlugin') +DHCP_HOSTA = 'dhcp-host-a' +DHCP_HOSTB = 'dhcp-host-b' class SegmentTestExtensionManager(object): @@ -894,3 +897,92 @@ class TestSegmentAwareIpamML2(TestSegmentAwareIpam): def setUp(self): super(TestSegmentAwareIpamML2, self).setUp( plugin='neutron.plugins.ml2.plugin.Ml2Plugin') + + +class TestDhcpAgentSegmentScheduling(HostSegmentMappingTestCase): + + _mechanism_drivers = ['openvswitch', 'logger'] + mock_path = 'neutron.services.segments.db.update_segment_host_mapping' + + def setUp(self): + super(TestDhcpAgentSegmentScheduling, self).setUp() + self.dhcp_agent_db = agentschedulers_db.DhcpAgentSchedulerDbMixin() + self.ctx = context.get_admin_context() + + def _test_create_network_and_segment(self, phys_net): + with self.network() as net: + network = net['network'] + segment = self._test_create_segment(network_id=network['id'], + physical_network=phys_net, + segmentation_id=200, + network_type='vxlan') + dhcp_agents = self.dhcp_agent_db.get_dhcp_agents_hosting_networks( + self.ctx, [network['id']]) + self.assertEqual(0, len(dhcp_agents)) + return network, segment['segment'] + + def _test_create_subnet(self, network, segment, cidr=None, + enable_dhcp=True): + cidr = cidr or '10.0.0.0/24' + ip_version = 4 + with self.subnet(network={'network': network}, + segment_id=segment['id'], + ip_version=ip_version, + cidr=cidr, + enable_dhcp=enable_dhcp) as subnet: + pass + return subnet['subnet'] + + def _register_dhcp_agents(self, hosts=None): + hosts = hosts or [DHCP_HOSTA, DHCP_HOSTB] + for host in hosts: + helpers.register_dhcp_agent(host) + + def test_network_scheduling_on_segment_creation(self): + self._register_dhcp_agents() + self._test_create_network_and_segment('phys_net1') + + def test_segment_scheduling_no_host_mapping(self): + self._register_dhcp_agents() + network, segment = self._test_create_network_and_segment('phys_net1') + self._test_create_subnet(network, segment) + dhcp_agents = self.dhcp_agent_db.get_dhcp_agents_hosting_networks( + self.ctx, [network['id']]) + self.assertEqual(0, len(dhcp_agents)) + + def test_segment_scheduling_with_host_mapping(self): + phys_net1 = 'phys_net1' + self._register_dhcp_agents() + network, segment = self._test_create_network_and_segment(phys_net1) + self._register_agent(DHCP_HOSTA, + mappings={phys_net1: 'br-eth-1'}, + plugin=self.plugin) + self._test_create_subnet(network, segment) + dhcp_agents = self.dhcp_agent_db.get_dhcp_agents_hosting_networks( + self.ctx, [network['id']]) + self.assertEqual(1, len(dhcp_agents)) + self.assertEqual(DHCP_HOSTA, dhcp_agents[0]['host']) + + def test_segment_scheduling_with_multiple_host_mappings(self): + phys_net1 = 'phys_net1' + phys_net2 = 'phys_net2' + self._register_dhcp_agents([DHCP_HOSTA, DHCP_HOSTB, 'MEHA', 'MEHB']) + network, segment1 = self._test_create_network_and_segment(phys_net1) + segment2 = self._test_create_segment(network_id=network['id'], + physical_network=phys_net2, + segmentation_id=200, + network_type='vxlan')['segment'] + self._register_agent(DHCP_HOSTA, + mappings={phys_net1: 'br-eth-1'}, + plugin=self.plugin) + self._register_agent(DHCP_HOSTB, + mappings={phys_net2: 'br-eth-1'}, + plugin=self.plugin) + self._test_create_subnet(network, segment1) + self._test_create_subnet(network, segment2, cidr='11.0.0.0/24') + dhcp_agents = self.dhcp_agent_db.get_dhcp_agents_hosting_networks( + self.ctx, [network['id']]) + self.assertEqual(2, len(dhcp_agents)) + agent_hosts = [agent['host'] for agent in dhcp_agents] + self.assertIn(DHCP_HOSTA, agent_hosts) + self.assertIn(DHCP_HOSTB, agent_hosts) diff --git a/neutron/tests/unit/scheduler/test_dhcp_agent_scheduler.py b/neutron/tests/unit/scheduler/test_dhcp_agent_scheduler.py index fc585c4f746..c4ed58a846f 100644 --- a/neutron/tests/unit/scheduler/test_dhcp_agent_scheduler.py +++ b/neutron/tests/unit/scheduler/test_dhcp_agent_scheduler.py @@ -28,6 +28,7 @@ from neutron.db import models_v2 from neutron.db.network_dhcp_agent_binding import models as ndab_model from neutron.extensions import dhcpagentscheduler from neutron.scheduler import dhcp_agent_scheduler +from neutron.services.segments import db as segments_service_db from neutron.tests.common import helpers from neutron.tests.unit.plugins.ml2 import test_plugin from neutron.tests.unit import testlib_api @@ -405,6 +406,8 @@ class DHCPAgentWeightSchedulerTestCase(test_plugin.Ml2PluginV2TestCase): self.plugin.network_scheduler = importutils.import_object( weight_scheduler) cfg.CONF.set_override("dhcp_load_type", "networks") + self.segments_plugin = importutils.import_object( + 'neutron.services.segments.plugin.Plugin') self.ctx = context.get_admin_context() def _create_network(self): @@ -416,6 +419,15 @@ class DHCPAgentWeightSchedulerTestCase(test_plugin.Ml2PluginV2TestCase): 'shared': True}}) return net['id'] + def _create_segment(self, network_id): + seg = self.segments_plugin.create_segment( + self.ctx, + {'segment': {'network_id': network_id, + 'physical_network': constants.ATTR_NOT_SPECIFIED, + 'network_type': 'meh', + 'segmentation_id': constants.ATTR_NOT_SPECIFIED}}) + return seg['id'] + def test_scheduler_one_agents_per_network(self): net_id = self._create_network() helpers.register_dhcp_agent(HOST_C) @@ -468,6 +480,85 @@ class DHCPAgentWeightSchedulerTestCase(test_plugin.Ml2PluginV2TestCase): self.assertEqual('host-c', agent2[0]['host']) self.assertEqual('host-d', agent3[0]['host']) + def test_schedule_segment_one_hostable_agent(self): + net_id = self._create_network() + seg_id = self._create_segment(net_id) + helpers.register_dhcp_agent(HOST_C) + helpers.register_dhcp_agent(HOST_D) + segments_service_db.update_segment_host_mapping( + self.ctx, HOST_C, {seg_id}) + net = self.plugin.get_network(self.ctx, net_id) + seg = self.segments_plugin.get_segment(self.ctx, seg_id) + net['candidate_hosts'] = seg['hosts'] + agents = self.plugin.network_scheduler.schedule( + self.plugin, self.ctx, net) + self.assertEqual(1, len(agents)) + self.assertEqual(HOST_C, agents[0].host) + + def test_schedule_segment_many_hostable_agents(self): + net_id = self._create_network() + seg_id = self._create_segment(net_id) + helpers.register_dhcp_agent(HOST_C) + helpers.register_dhcp_agent(HOST_D) + segments_service_db.update_segment_host_mapping( + self.ctx, HOST_C, {seg_id}) + segments_service_db.update_segment_host_mapping( + self.ctx, HOST_D, {seg_id}) + net = self.plugin.get_network(self.ctx, net_id) + seg = self.segments_plugin.get_segment(self.ctx, seg_id) + net['candidate_hosts'] = seg['hosts'] + agents = self.plugin.network_scheduler.schedule( + self.plugin, self.ctx, net) + self.assertEqual(1, len(agents)) + self.assertIn(agents[0].host, [HOST_C, HOST_D]) + + def test_schedule_segment_no_host_mapping(self): + net_id = self._create_network() + seg_id = self._create_segment(net_id) + helpers.register_dhcp_agent(HOST_C) + helpers.register_dhcp_agent(HOST_D) + net = self.plugin.get_network(self.ctx, net_id) + seg = self.segments_plugin.get_segment(self.ctx, seg_id) + net['candidate_hosts'] = seg['hosts'] + agents = self.plugin.network_scheduler.schedule( + self.plugin, self.ctx, net) + self.assertEqual(0, len(agents)) + + def test_schedule_segment_two_agents_per_segment(self): + cfg.CONF.set_override('dhcp_agents_per_network', 2) + net_id = self._create_network() + seg_id = self._create_segment(net_id) + helpers.register_dhcp_agent(HOST_C) + helpers.register_dhcp_agent(HOST_D) + segments_service_db.update_segment_host_mapping( + self.ctx, HOST_C, {seg_id}) + segments_service_db.update_segment_host_mapping( + self.ctx, HOST_D, {seg_id}) + net = self.plugin.get_network(self.ctx, net_id) + seg = self.segments_plugin.get_segment(self.ctx, seg_id) + net['candidate_hosts'] = seg['hosts'] + agents = self.plugin.network_scheduler.schedule( + self.plugin, self.ctx, net) + self.assertEqual(2, len(agents)) + self.assertIn(agents[0].host, [HOST_C, HOST_D]) + self.assertIn(agents[1].host, [HOST_C, HOST_D]) + + def test_schedule_segment_two_agents_per_segment_one_hostable_agent(self): + cfg.CONF.set_override('dhcp_agents_per_network', 2) + net_id = self._create_network() + seg_id = self._create_segment(net_id) + helpers.register_dhcp_agent(HOST_C) + helpers.register_dhcp_agent(HOST_D) + segments_service_db.update_segment_host_mapping( + self.ctx, HOST_C, {seg_id}) + net = self.plugin.get_network(self.ctx, net_id) + seg = self.segments_plugin.get_segment(self.ctx, seg_id) + net['candidate_hosts'] = seg['hosts'] + agents = self.plugin.network_scheduler.schedule( + self.plugin, self.ctx, net) + self.assertEqual(1, len(agents)) + self.assertEqual(HOST_C, agents[0].host) + class TestDhcpSchedulerFilter(TestDhcpSchedulerBaseTestCase, sched_db.DhcpAgentSchedulerDbMixin): @@ -518,6 +609,10 @@ class TestDhcpSchedulerFilter(TestDhcpSchedulerBaseTestCase, 'host-c', 'host-d'}, networks=networks) + def test_get_dhcp_agents_host_network_filter_by_hosts(self): + self._test_get_dhcp_agents_hosting_networks({'host-a'}, + hosts=['host-a']) + class DHCPAgentAZAwareWeightSchedulerTestCase(TestDhcpSchedulerBaseTestCase):