# Copyright 2014 OpenStack Foundation
#
# 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 datetime
from operator import attrgetter
import random
from unittest import mock

from neutron_lib import constants
from neutron_lib import context
from neutron_lib.exceptions import dhcpagentscheduler as das_exc
from oslo_config import cfg
from oslo_utils import importutils
from oslo_utils import uuidutils
import testscenarios

from neutron.db import agentschedulers_db as sched_db
from neutron.objects import agent
from neutron.objects import network as network_obj
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

# Required to generate tests from scenarios. Not compatible with nose.
load_tests = testscenarios.load_tests_apply_scenarios

HOST_C = 'host-c'
HOST_D = 'host-d'


class TestDhcpSchedulerBaseTestCase(testlib_api.SqlTestCase):
    CORE_PLUGIN = 'neutron.db.db_base_plugin_v2.NeutronDbPluginV2'

    def setUp(self):
        super().setUp()
        self.setup_coreplugin(self.CORE_PLUGIN)
        self.ctx = context.get_admin_context()
        self.network = {'id': uuidutils.generate_uuid()}
        self.network_id = self.network['id']
        self._save_networks([self.network_id])

    def _create_and_set_agents_down(self, hosts, down_agent_count=0,
                                    admin_state_up=True,
                                    az=helpers.DEFAULT_AZ):
        agents = []
        for i, host in enumerate(hosts):
            is_alive = i >= down_agent_count
            agents.append(helpers.register_dhcp_agent(
                host,
                admin_state_up=admin_state_up,
                alive=is_alive,
                az=az))
        return agents

    def _save_networks(self, networks):
        for network_id in networks:
            network_obj.Network(self.ctx, id=network_id).create()

    def _test_schedule_bind_network(self, agents, network_id):
        cfg.CONF.set_override('dhcp_agents_per_network', len(agents))
        scheduler = dhcp_agent_scheduler.ChanceScheduler()
        scheduler.resource_filter.bind(self.ctx, agents, network_id)
        binding_objs = network_obj.NetworkDhcpAgentBinding.get_objects(
            self.ctx, network_id=network_id)
        self.assertEqual(len(agents), len(binding_objs))
        for result in binding_objs:
            self.assertEqual(network_id, result.network_id)


class TestDhcpScheduler(TestDhcpSchedulerBaseTestCase):

    def test_schedule_bind_network_single_agent(self):
        agents = self._create_and_set_agents_down(['host-a'])
        self._test_schedule_bind_network(agents, self.network_id)

    def test_schedule_bind_network_multi_agents(self):
        agents = self._create_and_set_agents_down(['host-a', 'host-b'])
        self._test_schedule_bind_network(agents, self.network_id)

    def test_schedule_bind_network_multi_agent_fail_one(self):
        agents = self._create_and_set_agents_down(['host-a'])
        self._test_schedule_bind_network(agents, self.network_id)
        with mock.patch.object(dhcp_agent_scheduler.LOG, 'debug') as fake_log:
            self._test_schedule_bind_network(agents, self.network_id)
            self.assertEqual(1, fake_log.call_count)

    def _test_get_agents_and_scheduler_for_dead_agent(self):
        agents = self._create_and_set_agents_down(['dead_host', 'alive_host'],
                                                  1)
        dead_agent = [agents[0]]
        alive_agent = [agents[1]]
        self._test_schedule_bind_network(dead_agent, self.network_id)
        scheduler = dhcp_agent_scheduler.ChanceScheduler()
        return dead_agent, alive_agent, scheduler

    def _test_reschedule_vs_network_on_dead_agent(self,
                                                  active_hosts_only):
        dead_agent, alive_agent, scheduler = (
            self._test_get_agents_and_scheduler_for_dead_agent())
        network = {'id': self.network_id}
        plugin = mock.Mock()
        plugin.get_subnets.return_value = [{"network_id": self.network_id,
                                            "enable_dhcp": True}]
        plugin.get_agent_objects.return_value = dead_agent + alive_agent
        plugin.filter_hosts_with_network_access.side_effect = (
            lambda context, network_id, hosts: hosts)
        if active_hosts_only:
            plugin.get_dhcp_agents_hosting_networks.return_value = []
            self.assertTrue(
                scheduler.schedule(
                    plugin, self.ctx, network))
        else:
            plugin.get_dhcp_agents_hosting_networks.return_value = dead_agent
            self.assertFalse(
                scheduler.schedule(
                    plugin, self.ctx, network))

    def test_network_rescheduled_when_db_returns_active_hosts(self):
        self._test_reschedule_vs_network_on_dead_agent(True)

    def test_network_not_rescheduled_when_db_returns_all_hosts(self):
        self._test_reschedule_vs_network_on_dead_agent(False)

    def _get_agent_binding_from_db(self, agent):
        return network_obj.NetworkDhcpAgentBinding.get_objects(
            self.ctx, dhcp_agent_id=agent[0].id)

    def test_auto_reschedule_vs_network_on_dead_agent(self):
        dead_agent, alive_agent, scheduler = (
            self._test_get_agents_and_scheduler_for_dead_agent())
        plugin = mock.Mock()
        plugin.get_subnets.return_value = [{"network_id": self.network_id,
                                            "enable_dhcp": True,
                                            "segment_id": None}]
        plugin.get_network.return_value = self.network
        plugin.get_dhcp_agents_hosting_networks.return_value = dead_agent
        network_assigned_to_dead_agent = (
            self._get_agent_binding_from_db(dead_agent))
        self.assertEqual(1, len(network_assigned_to_dead_agent))
        self.assertTrue(
            scheduler.auto_schedule_networks(
                plugin, self.ctx, "alive_host"))
        network_assigned_to_dead_agent = (
            self._get_agent_binding_from_db(dead_agent))
        network_assigned_to_alive_agent = (
            self._get_agent_binding_from_db(alive_agent))
        self.assertEqual(1, len(network_assigned_to_dead_agent))
        # network won't be scheduled to new agent unless removed from
        # dead agent
        self.assertEqual(0, len(network_assigned_to_alive_agent))


class TestAutoScheduleNetworks(TestDhcpSchedulerBaseTestCase):
    """Unit test scenarios for ChanceScheduler.auto_schedule_networks.

    network_present
        Network is present or not

    enable_dhcp
        Dhcp is enabled or disabled in the subnet of the network

    scheduled_already
        Network is already scheduled to the agent or not

    agent_down
        Dhcp agent is down or alive

    valid_host
        If true, then an valid host is passed to schedule the network,
        else an invalid host is passed.

    az_hints
        'availability_zone_hints' of the network.
        note that default 'availability_zone' of an agent is 'nova'.
    """
    scenarios = [
        ('Network present',
         dict(network_present=True,
              enable_dhcp=True,
              scheduled_already=False,
              agent_down=False,
              valid_host=True,
              az_hints=[])),

        ('No network',
         dict(network_present=False,
              enable_dhcp=False,
              scheduled_already=False,
              agent_down=False,
              valid_host=True,
              az_hints=[])),

        ('Network already scheduled',
         dict(network_present=True,
              enable_dhcp=True,
              scheduled_already=True,
              agent_down=False,
              valid_host=True,
              az_hints=[])),

        ('Agent down',
         dict(network_present=True,
              enable_dhcp=True,
              scheduled_already=False,
              agent_down=False,
              valid_host=True,
              az_hints=[])),

        ('dhcp disabled',
         dict(network_present=True,
              enable_dhcp=False,
              scheduled_already=False,
              agent_down=False,
              valid_host=False,
              az_hints=[])),

        ('Invalid host',
         dict(network_present=True,
              enable_dhcp=True,
              scheduled_already=False,
              agent_down=False,
              valid_host=False,
              az_hints=[])),

        ('Match AZ',
         dict(network_present=True,
              enable_dhcp=True,
              scheduled_already=False,
              agent_down=False,
              valid_host=True,
              az_hints=['nova'])),

        ('Not match AZ',
         dict(network_present=True,
              enable_dhcp=True,
              scheduled_already=False,
              agent_down=False,
              valid_host=True,
              az_hints=['not-match'])),
    ]

    def test_auto_schedule_network(self):
        plugin = mock.MagicMock()
        plugin.get_subnets.return_value = (
            [{"network_id": self.network_id, "enable_dhcp": self.enable_dhcp,
              "segment_id": None}] if self.network_present else [])
        plugin.get_network.return_value = {'availability_zone_hints':
                                           self.az_hints}
        scheduler = dhcp_agent_scheduler.ChanceScheduler()
        if self.network_present:
            down_agent_count = 1 if self.agent_down else 0
            agents = self._create_and_set_agents_down(
                ['host-a'], down_agent_count=down_agent_count)
            if self.scheduled_already:
                self._test_schedule_bind_network(agents, self.network_id)

        expected_result = (self.network_present and self.enable_dhcp)
        expected_hosted_agents = (1 if expected_result and
                                  self.valid_host else 0)
        if (self.az_hints and
                agents[0]['availability_zone'] not in self.az_hints):
            expected_hosted_agents = 0
        host = "host-a" if self.valid_host else "host-b"
        observed_ret_value = scheduler.auto_schedule_networks(
            plugin, self.ctx, host)
        self.assertEqual(expected_result, observed_ret_value)
        count_hosted_agents = network_obj.NetworkDhcpAgentBinding.count(
            self.ctx)
        self.assertEqual(expected_hosted_agents, count_hosted_agents)


class TestAutoScheduleSegments(test_plugin.Ml2PluginV2TestCase,
                               TestDhcpSchedulerBaseTestCase):
    """Unit test scenarios for ChanceScheduler"""
    CORE_PLUGIN = 'neutron.plugins.ml2.plugin.Ml2Plugin'

    def setUp(self):
        super().setUp()
        self.plugin = self.driver
        self.segments_plugin = importutils.import_object(
            'neutron.services.segments.plugin.Plugin')
        self.ctx = context.get_admin_context()
        # Remove MissingAuthPlugin exception from logs
        mock.patch(
            'neutron.notifiers.batch_notifier.BatchNotifier._notify').start()

    def _create_network(self):
        net = self.plugin.create_network(
            self.ctx,
            {'network': {'name': 'name',
                         'tenant_id': 'tenant_one',
                         'admin_state_up': True,
                         'shared': True}})
        return net['id']

    def _create_segment(self, network_id, physical_network='physnet1'):
        seg = self.segments_plugin.create_segment(
            self.ctx,
            {'segment': {'network_id': network_id,
                         'name': None, 'description': None,
                         'physical_network': physical_network,
                         'network_type': 'vlan',
                         'segmentation_id': constants.ATTR_NOT_SPECIFIED}})
        return seg['id']

    def _create_subnet(self, segment_id, network_id, cidr='192.168.10.0/24'):
        subnet = self.plugin.create_subnet(
            self.ctx,
            {'subnet': {'name': 'name',
                        'ip_version': constants.IP_VERSION_4,
                        'network_id': network_id,
                        'cidr': cidr,
                        'gateway_ip': constants.ATTR_NOT_SPECIFIED,
                        'allocation_pools': constants.ATTR_NOT_SPECIFIED,
                        'dns_nameservers': constants.ATTR_NOT_SPECIFIED,
                        'host_routes': constants.ATTR_NOT_SPECIFIED,
                        'tenant_id': 'tenant_one',
                        'enable_dhcp': True,
                        'segment_id': segment_id}})
        return subnet['id']

    def test_auto_schedule_one_network_one_segment_one_subnet(self):
        net_id = self._create_network()
        seg_id = self._create_segment(net_id)
        self._create_subnet(seg_id, net_id)
        helpers.register_dhcp_agent(HOST_C)
        segments_service_db.update_segment_host_mapping(
            self.ctx, HOST_C, {seg_id})
        scheduler = dhcp_agent_scheduler.ChanceScheduler()
        observed_return_val = scheduler.auto_schedule_networks(
            self.plugin, self.ctx, HOST_C)
        self.assertTrue(observed_return_val)
        agent1 = self.plugin.get_dhcp_agents_hosting_networks(
            self.ctx, [net_id])
        self.assertEqual(1, len(agent1))
        self.assertEqual('host-c', agent1[0]['host'])

    def test_auto_schedule_one_network_one_segment_two_subnet(self):
        net_id = self._create_network()
        seg_id = self._create_segment(net_id)
        self._create_subnet(seg_id, net_id)
        self._create_subnet(seg_id, net_id, '192.168.11.0/24')
        helpers.register_dhcp_agent(HOST_C)
        segments_service_db.update_segment_host_mapping(
            self.ctx, HOST_C, {seg_id})
        scheduler = dhcp_agent_scheduler.ChanceScheduler()
        observed_return_val = scheduler.auto_schedule_networks(
            self.plugin, self.ctx, HOST_C)
        self.assertTrue(observed_return_val)
        agent1 = self.plugin.get_dhcp_agents_hosting_networks(
            self.ctx, [net_id])
        self.assertEqual(1, len(agent1))
        self.assertEqual('host-c', agent1[0]['host'])

    def test_auto_schedule_one_network_two_segments_with_one_subnet_each(self):
        net_id = self._create_network()
        seg1_id = self._create_segment(net_id)
        self._create_subnet(seg1_id, net_id)
        helpers.register_dhcp_agent(HOST_D)
        segments_service_db.update_segment_host_mapping(
            self.ctx, HOST_D, {seg1_id})
        scheduler = dhcp_agent_scheduler.ChanceScheduler()
        observed_val_first_segment = scheduler.auto_schedule_networks(
            self.plugin, self.ctx, HOST_D)
        self.assertTrue(observed_val_first_segment)
        agents = self.plugin.get_dhcp_agents_hosting_networks(
            self.ctx, [net_id])
        self.assertEqual(1, len(agents))

        cfg.CONF.set_override('dhcp_agents_per_network', 2)
        seg2_id = self._create_segment(net_id, 'physnet2')
        self._create_subnet(seg2_id, net_id, '192.168.11.0/24')
        helpers.register_dhcp_agent(HOST_C)
        segments_service_db.update_segment_host_mapping(
            self.ctx, HOST_C, {seg2_id})
        observed_val_second_segment = scheduler.auto_schedule_networks(
            self.plugin, self.ctx, HOST_C)
        self.assertTrue(observed_val_second_segment)
        agents = self.plugin.get_dhcp_agents_hosting_networks(
            self.ctx, [net_id])
        self.assertEqual(2, len(agents))


class TestNetworksFailover(TestDhcpSchedulerBaseTestCase,
                           sched_db.DhcpAgentSchedulerDbMixin):
    def test_auto_schedule_network_excess_agents(self):
        plugin = mock.MagicMock()
        plugin.get_subnets.return_value = (
            [{"network_id": self.network_id, "enable_dhcp": True}])
        plugin.get_network.return_value = {'availability_zone_hints': ['nova']}
        scheduler = dhcp_agent_scheduler.ChanceScheduler()
        dhcpfilter = 'neutron.scheduler.dhcp_agent_scheduler.DhcpFilter'
        self._create_and_set_agents_down(['host-a', 'host-b'])
        expected_hosted_agents = 1
        binding_index = 1
        scheduler.auto_schedule_networks(plugin, self.ctx, 'host-a')
        with mock.patch(
                dhcpfilter + '.get_vacant_network_dhcp_agent_binding_index',
                context=self.ctx, network_id=self.network_id) as ndab:
            ndab.return_value = binding_index
            scheduler.auto_schedule_networks(plugin, self.ctx, 'host-b')
            self.assertTrue(ndab.called)
        num_hosted_agents = network_obj.NetworkDhcpAgentBinding.count(
            self.ctx, network_id=self.network_id)
        self.assertEqual(expected_hosted_agents, num_hosted_agents)

    def test_reschedule_network_from_down_agent(self):
        net_id = uuidutils.generate_uuid()
        agents = self._create_and_set_agents_down(['host-a', 'host-b'], 1)
        self._test_schedule_bind_network([agents[0]], self.network_id)
        self._save_networks([net_id])
        self._test_schedule_bind_network([agents[1]], net_id)
        with mock.patch.object(self, 'remove_network_from_dhcp_agent') as rn,\
                mock.patch.object(self,
                                  'schedule_network',
                                  return_value=[agents[1]]) as sch,\
                mock.patch.object(self,
                                  'get_network',
                                  create=True,
                                  return_value={'id': self.network_id}):
            notifier = mock.MagicMock()
            self.agent_notifiers[constants.AGENT_TYPE_DHCP] = notifier
            self.remove_networks_from_down_agents()
            rn.assert_called_with(mock.ANY, agents[0].id, self.network_id,
                                  notify=False)
            sch.assert_called_with(mock.ANY, {'id': self.network_id})
            notifier.network_added_to_agent.assert_called_with(
                mock.ANY, self.network_id, agents[1].host)

    def _test_failed_rescheduling(self, rn_side_effect=None):
        agents = self._create_and_set_agents_down(['host-a', 'host-b'], 1)
        self._test_schedule_bind_network([agents[0]], self.network_id)
        with mock.patch.object(self,
                               'remove_network_from_dhcp_agent',
                               side_effect=rn_side_effect) as rn,\
                mock.patch.object(self,
                                  'schedule_network',
                                  return_value=None) as sch,\
                mock.patch.object(self,
                                  'get_network',
                                  create=True,
                                  return_value={'id': self.network_id}):
            notifier = mock.MagicMock()
            self.agent_notifiers[constants.AGENT_TYPE_DHCP] = notifier
            self.remove_networks_from_down_agents()
            rn.assert_called_with(mock.ANY, agents[0].id, self.network_id,
                                  notify=False)
            sch.assert_called_with(mock.ANY, {'id': self.network_id})
            self.assertFalse(notifier.network_added_to_agent.called)

    def test_reschedule_network_from_down_agent_failed(self):
        self._test_failed_rescheduling()

    def test_reschedule_network_from_down_agent_concurrent_removal(self):
        self._test_failed_rescheduling(
            rn_side_effect=das_exc.NetworkNotHostedByDhcpAgent(
                network_id='foo', agent_id='bar'))

    def _create_test_networks(self, num_net=0):
        networks = [network_obj.Network(
                       self.ctx,
                       id=uuidutils.generate_uuid(),
                       name='network-%s' % (i))
                    for i in range(num_net)]
        for net in networks:
            net.create()
        return [net.id for net in networks]

    def _create_dhcp_agents(self):
        timestamp = datetime.datetime.now()
        dhcp_agent_ids = [uuidutils.generate_uuid() for x in range(2)]
        dhcp_agent_1 = agent.Agent(self.ctx, id=dhcp_agent_ids[0],
                                   agent_type='DHCP Agent',
                                   topic='fake_topic',
                                   host='fake_host',
                                   binary='fake_binary',
                                   created_at=timestamp,
                                   started_at=timestamp,
                                   heartbeat_timestamp=timestamp,
                                   configurations={},
                                   load=0)
        dhcp_agent_1.create()

        dhcp_agent_2 = agent.Agent(self.ctx, id=dhcp_agent_ids[1],
                                   agent_type='DHCP Agent',
                                   topic='fake_topic',
                                   host='fake_host_1',
                                   binary='fake_binary',
                                   created_at=timestamp,
                                   started_at=timestamp,
                                   heartbeat_timestamp=timestamp,
                                   configurations={},
                                   load=0)
        dhcp_agent_2.create()
        return [dhcp_agent_1.id, dhcp_agent_2.id]

    def test_filter_bindings(self):
        self.ctx = context.get_admin_context()
        dhcp_agt_ids = self._create_dhcp_agents()
        network_ids = sorted(self._create_test_networks(num_net=4))
        ndab_obj1 = network_obj.NetworkDhcpAgentBinding(
            self.ctx, network_id=network_ids[0], dhcp_agent_id=dhcp_agt_ids[0])
        ndab_obj1.create()
        ndab_obj2 = network_obj.NetworkDhcpAgentBinding(
            self.ctx, network_id=network_ids[1], dhcp_agent_id=dhcp_agt_ids[0])
        ndab_obj2.create()
        ndab_obj3 = network_obj.NetworkDhcpAgentBinding(
            self.ctx, network_id=network_ids[2], dhcp_agent_id=dhcp_agt_ids[1])
        ndab_obj3.create()
        ndab_obj4 = network_obj.NetworkDhcpAgentBinding(
            self.ctx, network_id=network_ids[3], dhcp_agent_id=dhcp_agt_ids[1])
        ndab_obj4.create()
        bindings_objs = sorted(network_obj.NetworkDhcpAgentBinding.get_objects(
            self.ctx), key=attrgetter('network_id'))

        with mock.patch.object(self, 'agent_starting_up',
                               side_effect=[True, False]):
            res = list(self._filter_bindings(None, bindings_objs))
            # once per each agent id1 and id2
            self.assertEqual(2, len(res))
            res_ids = [b.network_id for b in res]
            self.assertIn(network_ids[2], res_ids)
            self.assertIn(network_ids[3], res_ids)

    def test_reschedule_network_from_down_agent_failed_on_unexpected(self):
        agents = self._create_and_set_agents_down(['host-a'], 1)
        self._test_schedule_bind_network([agents[0]], self.network_id)
        with mock.patch.object(self, '_filter_bindings',
                               side_effect=Exception()):
            # just make sure that no exception is raised
            self.remove_networks_from_down_agents()

    def test_reschedule_network_catches_exceptions_on_fetching_bindings(self):
        with mock.patch('neutron_lib.context.get_admin_context') as get_ctx:
            mock_ctx = mock.Mock()
            get_ctx.return_value = mock_ctx
            mock_ctx.session.query.side_effect = Exception()
            # just make sure that no exception is raised
            self.remove_networks_from_down_agents()

    def test_reschedule_doesnt_occur_if_no_agents(self):
        agents = self._create_and_set_agents_down(['host-a', 'host-b'], 2)
        self._test_schedule_bind_network([agents[0]], self.network_id)
        with mock.patch.object(self, 'remove_network_from_dhcp_agent') as rn:
            self.remove_networks_from_down_agents()
            self.assertFalse(rn.called)


class DHCPAgentWeightSchedulerTestCase(test_plugin.Ml2PluginV2TestCase):
    """Unit test scenarios for WeightScheduler.schedule."""

    def setUp(self):
        super().setUp()
        weight_scheduler = (
            'neutron.scheduler.dhcp_agent_scheduler.WeightScheduler')
        cfg.CONF.set_override('network_scheduler_driver', weight_scheduler)
        self.plugin = self.driver
        mock.patch.object(
            self.plugin, 'filter_hosts_with_network_access',
            side_effect=lambda context, network_id, hosts: hosts).start()
        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):
        net = self.plugin.create_network(
            self.ctx,
            {'network': {'name': 'name',
                         'tenant_id': 'tenant_one',
                         'admin_state_up': True,
                         'shared': True}})
        return net['id']

    def _create_segment(self, network_id):
        seg = self.segments_plugin.create_segment(
            self.ctx,
            {'segment': {'network_id': network_id,
                         'name': None, 'description': None,
                         'physical_network': 'physnet1',
                         'network_type': 'vlan',
                         '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)
        self.plugin.network_scheduler.schedule(self.plugin, self.ctx,
                                               {'id': net_id})
        agents = self.plugin.get_dhcp_agents_hosting_networks(self.ctx,
                                                              [net_id])
        self.assertEqual(1, len(agents))

    def test_scheduler_two_agents_per_network(self):
        cfg.CONF.set_override('dhcp_agents_per_network', 2)
        net_id = self._create_network()
        helpers.register_dhcp_agent(HOST_C)
        helpers.register_dhcp_agent(HOST_D)
        self.plugin.network_scheduler.schedule(self.plugin, self.ctx,
                                               {'id': net_id})
        agents = self.plugin.get_dhcp_agents_hosting_networks(self.ctx,
                                                              [net_id])
        self.assertEqual(2, len(agents))

    def test_scheduler_no_active_agents(self):
        net_id = self._create_network()
        self.plugin.network_scheduler.schedule(self.plugin, self.ctx,
                                               {'id': net_id})
        agents = self.plugin.get_dhcp_agents_hosting_networks(self.ctx,
                                                              [net_id])
        self.assertEqual(0, len(agents))

    def test_scheduler_equal_distribution(self):
        net_id_1 = self._create_network()
        net_id_2 = self._create_network()
        net_id_3 = self._create_network()
        helpers.register_dhcp_agent(HOST_C)
        helpers.register_dhcp_agent(HOST_D, networks=1)
        self.plugin.network_scheduler.schedule(
            self.plugin, context.get_admin_context(), {'id': net_id_1})
        helpers.register_dhcp_agent(HOST_D, networks=2)
        self.plugin.network_scheduler.schedule(
            self.plugin, context.get_admin_context(), {'id': net_id_2})
        helpers.register_dhcp_agent(HOST_C, networks=4)
        self.plugin.network_scheduler.schedule(
            self.plugin, context.get_admin_context(), {'id': net_id_3})
        agent1 = self.plugin.get_dhcp_agents_hosting_networks(
            self.ctx, [net_id_1])
        agent2 = self.plugin.get_dhcp_agents_hosting_networks(
            self.ctx, [net_id_2])
        agent3 = self.plugin.get_dhcp_agents_hosting_networks(
            self.ctx, [net_id_3])
        self.assertEqual('host-c', agent1[0]['host'])
        self.assertEqual('host-c', agent2[0]['host'])
        self.assertEqual('host-d', agent3[0]['host'])

    def _get_network_with_candidate_hosts(self, net_id, seg_id):
        # expire the session so that the segment is fully reloaded on fetch,
        # including its new host mapping
        self.ctx.session.expire_all()
        net = self.plugin.get_network(self.ctx, net_id)
        seg = self.segments_plugin.get_segment(self.ctx, seg_id)
        net['candidate_hosts'] = seg['hosts']
        return net

    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._get_network_with_candidate_hosts(net_id, seg_id)
        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._get_network_with_candidate_hosts(net_id, seg_id)
        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._get_network_with_candidate_hosts(net_id, seg_id)
        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._get_network_with_candidate_hosts(net_id, seg_id)
        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):
    def _test_get_dhcp_agents_hosting_networks(self, expected, **kwargs):
        cfg.CONF.set_override('dhcp_agents_per_network', 4)
        agents = self._create_and_set_agents_down(['host-a', 'host-b'], 1)
        agents += self._create_and_set_agents_down(['host-c', 'host-d'], 1,
                                                   admin_state_up=False)
        networks = kwargs.pop('networks', [self.network_id])
        for network in networks:
            self._test_schedule_bind_network(agents, network)
        agents = self.get_dhcp_agents_hosting_networks(self.ctx,
                                                       networks,
                                                       **kwargs)
        host_ids = {a['host'] for a in agents}
        self.assertEqual(expected, host_ids)

    def test_get_dhcp_agents_hosting_networks_default(self):
        self._test_get_dhcp_agents_hosting_networks({'host-a', 'host-b',
                                                     'host-c', 'host-d'})

    def test_get_dhcp_agents_hosting_networks_active(self):
        self._test_get_dhcp_agents_hosting_networks({'host-b', 'host-d'},
                                                    active=True)

    def test_get_dhcp_agents_hosting_networks_admin_up(self):
        self._test_get_dhcp_agents_hosting_networks({'host-a', 'host-b'},
                                                    admin_state_up=True)

    def test_get_dhcp_agents_hosting_networks_active_admin_up(self):
        self._test_get_dhcp_agents_hosting_networks({'host-b'},
                                                    active=True,
                                                    admin_state_up=True)

    def test_get_dhcp_agents_hosting_networks_admin_down(self):
        self._test_get_dhcp_agents_hosting_networks({'host-c', 'host-d'},
                                                    admin_state_up=False)

    def test_get_dhcp_agents_hosting_networks_active_admin_down(self):
        self._test_get_dhcp_agents_hosting_networks({'host-d'},
                                                    active=True,
                                                    admin_state_up=False)

    def test_get_dhcp_agents_hosting_many_networks(self):
        net_id = uuidutils.generate_uuid()
        self._save_networks([net_id])
        networks = [net_id, self.network_id]
        self._test_get_dhcp_agents_hosting_networks({'host-a', 'host-b',
                                                     '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):

    def setUp(self):
        super().setUp()
        self.setup_coreplugin('ml2')
        cfg.CONF.set_override("network_scheduler_driver",
            'neutron.scheduler.dhcp_agent_scheduler.AZAwareWeightScheduler')
        self.plugin = importutils.import_object('neutron.plugins.ml2.plugin.'
                                                'Ml2Plugin')
        mock.patch.object(
            self.plugin, 'filter_hosts_with_network_access',
            side_effect=lambda context, network_id, hosts: hosts).start()
        cfg.CONF.set_override('dhcp_agents_per_network', 1)
        cfg.CONF.set_override("dhcp_load_type", "networks")

    def test_az_scheduler_one_az_hints(self):
        net_id = uuidutils.generate_uuid()
        self._save_networks([net_id])
        helpers.register_dhcp_agent('az1-host1', networks=1, az='az1')
        helpers.register_dhcp_agent('az1-host2', networks=2, az='az1')
        helpers.register_dhcp_agent('az2-host1', networks=3, az='az2')
        helpers.register_dhcp_agent('az2-host2', networks=4, az='az2')
        self.plugin.network_scheduler.schedule(
            self.plugin, self.ctx,
            {'id': net_id, 'availability_zone_hints': ['az2']})
        agents = self.plugin.get_dhcp_agents_hosting_networks(self.ctx,
                                                              [net_id])
        self.assertEqual(1, len(agents))
        self.assertEqual('az2-host1', agents[0]['host'])

    def test_az_scheduler_default_az_hints(self):
        net_id = uuidutils.generate_uuid()
        cfg.CONF.set_override('default_availability_zones', ['az1'])
        self._save_networks([net_id])
        helpers.register_dhcp_agent('az1-host1', networks=1, az='az1')
        helpers.register_dhcp_agent('az1-host2', networks=2, az='az1')
        helpers.register_dhcp_agent('az2-host1', networks=3, az='az2')
        helpers.register_dhcp_agent('az2-host2', networks=4, az='az2')
        self.plugin.network_scheduler.schedule(
            self.plugin, self.ctx,
            {'id': net_id, 'availability_zone_hints': []})
        agents = self.plugin.get_dhcp_agents_hosting_networks(self.ctx,
                                                              [net_id])
        self.assertEqual(1, len(agents))
        self.assertEqual('az1-host1', agents[0]['host'])

    def test_az_scheduler_two_az_hints(self):
        net_id = uuidutils.generate_uuid()
        cfg.CONF.set_override('dhcp_agents_per_network', 2)
        self._save_networks([net_id])
        helpers.register_dhcp_agent('az1-host1', networks=1, az='az1')
        helpers.register_dhcp_agent('az1-host2', networks=2, az='az1')
        helpers.register_dhcp_agent('az2-host1', networks=3, az='az2')
        helpers.register_dhcp_agent('az2-host2', networks=4, az='az2')
        helpers.register_dhcp_agent('az3-host1', networks=5, az='az3')
        helpers.register_dhcp_agent('az3-host2', networks=6, az='az3')
        self.plugin.network_scheduler.schedule(
            self.plugin, self.ctx,
            {'id': net_id, 'availability_zone_hints': ['az1', 'az3']})
        agents = self.plugin.get_dhcp_agents_hosting_networks(self.ctx,
                                                              [net_id])
        self.assertEqual(2, len(agents))
        expected_hosts = {'az1-host1', 'az3-host1'}
        hosts = {a['host'] for a in agents}
        self.assertEqual(expected_hosts, hosts)

    def test_az_scheduler_two_az_hints_one_available_az(self):
        net_id = uuidutils.generate_uuid()
        cfg.CONF.set_override('dhcp_agents_per_network', 2)
        self._save_networks([net_id])
        helpers.register_dhcp_agent('az1-host1', networks=1, az='az1')
        helpers.register_dhcp_agent('az1-host2', networks=2, az='az1')
        helpers.register_dhcp_agent('az2-host1', networks=3, alive=False,
                                    az='az2')
        helpers.register_dhcp_agent('az2-host2', networks=4,
                                    admin_state_up=False, az='az2')
        self.plugin.network_scheduler.schedule(
            self.plugin, self.ctx,
            {'id': net_id, 'availability_zone_hints': ['az1', 'az2']})
        agents = self.plugin.get_dhcp_agents_hosting_networks(self.ctx,
                                                              [net_id])
        self.assertEqual(2, len(agents))
        expected_hosts = {'az1-host1', 'az1-host2'}
        hosts = {a['host'] for a in agents}
        self.assertEqual(expected_hosts, hosts)

    def _test_az_scheduler_no_az_hints(self, multiple_agent=False):
        net_id = uuidutils.generate_uuid()
        num_agent = 2 if multiple_agent else 1
        cfg.CONF.set_override('dhcp_agents_per_network', num_agent)
        self._save_networks([net_id])
        helpers.register_dhcp_agent('az1-host1', networks=2, az='az1')
        helpers.register_dhcp_agent('az1-host2', networks=3, az='az1')
        helpers.register_dhcp_agent('az2-host1', networks=2, az='az2')
        helpers.register_dhcp_agent('az2-host2', networks=1, az='az2')
        self.plugin.network_scheduler.schedule(
            self.plugin, self.ctx,
            {'id': net_id, 'availability_zone_hints': []})
        agents = self.plugin.get_dhcp_agents_hosting_networks(self.ctx,
                                                              [net_id])
        self.assertEqual(num_agent, len(agents))
        if multiple_agent:
            expected_hosts = {'az1-host1', 'az2-host2'}
        else:
            expected_hosts = {'az2-host2'}
        hosts = {a['host'] for a in agents}
        self.assertEqual(expected_hosts, hosts)

    def test_az_scheduler_no_az_hints_multiple_agent(self):
        self._test_az_scheduler_no_az_hints(multiple_agent=True)

    def test_az_scheduler_no_az_hints_one_agent(self):
        self._test_az_scheduler_no_az_hints()

    def test_az_scheduler_select_az_with_least_weight(self):
        self._save_networks([uuidutils.generate_uuid()])
        dhcp_agents = []
        # Register 6 dhcp agents in 3 AZs, every AZ will have 2 agents.
        dhcp_agents.append(
            helpers.register_dhcp_agent('az1-host1', networks=6, az='az1'))
        dhcp_agents.append(
            helpers.register_dhcp_agent('az1-host2', networks=5, az='az1'))
        dhcp_agents.append(
            helpers.register_dhcp_agent('az2-host1', networks=4, az='az2'))
        dhcp_agents.append(
            helpers.register_dhcp_agent('az2-host2', networks=3, az='az2'))
        dhcp_agents.append(
            helpers.register_dhcp_agent('az3-host1', networks=2, az='az3'))
        dhcp_agents.append(
            helpers.register_dhcp_agent('az3-host2', networks=1, az='az3'))

        # Try multiple times to verify that the select of AZ scheduler will
        # output stably.
        for i in range(3):
            # Shuffle the agents
            random.shuffle(dhcp_agents)
            # Select agents with empty resource_hosted_agents. This means each
            # AZ will have same amount of agents scheduled (0 in this case)
            agents_select = self.plugin.network_scheduler.select(
                self.plugin, self.ctx, dhcp_agents, [], 2)

            self.assertEqual(2, len(agents_select))
            # The agent and az with least weight should always be selected
            # first
            self.assertEqual('az3-host2', agents_select[0]['host'])
            self.assertEqual('az3', agents_select[0]['availability_zone'])
            # The second selected agent should be the agent with least weight,
            # which is also not in the same az as the first selected agent.
            self.assertEqual('az2-host2', agents_select[1]['host'])
            self.assertEqual('az2', agents_select[1]['availability_zone'])
