# vim: tabstop=4 shiftwidth=4 softtabstop=4

# Copyright 2012 OpenStack Foundation
# 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.

import copy
import os
import sys
import uuid

import eventlet
import mock
from oslo.config import cfg
import testtools

from neutron.agent.common import config
from neutron.agent import dhcp_agent
from neutron.agent.dhcp_agent import DhcpAgentWithStateReport
from neutron.agent.linux import dhcp
from neutron.agent.linux import interface
from neutron.common import constants as const
from neutron.common import exceptions
from neutron.tests import base


ROOTDIR = os.path.dirname(os.path.dirname(__file__))
ETCDIR = os.path.join(ROOTDIR, 'etc')
HOSTNAME = 'hostname'
dev_man = dhcp.DeviceManager
rpc_api = dhcp_agent.DhcpPluginApi
DEVICE_MANAGER = '%s.%s' % (dev_man.__module__, dev_man.__name__)
DHCP_PLUGIN = '%s.%s' % (rpc_api.__module__, rpc_api.__name__)


def etcdir(*p):
    return os.path.join(ETCDIR, *p)


fake_tenant_id = 'aaaaaaaa-aaaa-aaaa-aaaaaaaaaaaa'
fake_subnet1_allocation_pools = dhcp.DictModel(dict(id='', start='172.9.9.2',
                                               end='172.9.9.254'))
fake_subnet1 = dhcp.DictModel(dict(id='bbbbbbbb-bbbb-bbbb-bbbbbbbbbbbb',
                              network_id='12345678-1234-5678-1234567890ab',
                              cidr='172.9.9.0/24', enable_dhcp=True, name='',
                              tenant_id=fake_tenant_id,
                              gateway_ip='172.9.9.1', host_routes=[],
                              dns_nameservers=[], ip_version=4,
                              allocation_pools=fake_subnet1_allocation_pools))

fake_subnet2_allocation_pools = dhcp.DictModel(dict(id='', start='172.9.8.2',
                                               end='172.9.8.254'))
fake_subnet2 = dhcp.DictModel(dict(id='dddddddd-dddd-dddd-dddddddddddd',
                              network_id='12345678-1234-5678-1234567890ab',
                              cidr='172.9.8.0/24', enable_dhcp=False, name='',
                              tenant_id=fake_tenant_id, gateway_ip='172.9.8.1',
                              host_routes=[], dns_nameservers=[], ip_version=4,
                              allocation_pools=fake_subnet2_allocation_pools))

fake_subnet3 = dhcp.DictModel(dict(id='bbbbbbbb-1111-2222-bbbbbbbbbbbb',
                              network_id='12345678-1234-5678-1234567890ab',
                              cidr='192.168.1.1/24', enable_dhcp=True))

fake_meta_subnet = dhcp.DictModel(dict(id='bbbbbbbb-1111-2222-bbbbbbbbbbbb',
                                  network_id='12345678-1234-5678-1234567890ab',
                                  cidr='169.254.169.252/30',
                                  gateway_ip='169.254.169.253',
                                  enable_dhcp=True))

fake_fixed_ip1 = dhcp.DictModel(dict(id='', subnet_id=fake_subnet1.id,
                                ip_address='172.9.9.9'))
fake_meta_fixed_ip = dhcp.DictModel(dict(id='', subnet=fake_meta_subnet,
                                    ip_address='169.254.169.254'))
fake_allocation_pool_subnet1 = dhcp.DictModel(dict(id='', start='172.9.9.2',
                                              end='172.9.9.254'))

fake_port1 = dhcp.DictModel(dict(id='12345678-1234-aaaa-1234567890ab',
                            device_id='dhcp-12345678-1234-aaaa-1234567890ab',
                            allocation_pools=fake_subnet1_allocation_pools,
                            mac_address='aa:bb:cc:dd:ee:ff',
                            network_id='12345678-1234-5678-1234567890ab',
                            fixed_ips=[fake_fixed_ip1]))

fake_port2 = dhcp.DictModel(dict(id='12345678-1234-aaaa-123456789000',
                            mac_address='aa:bb:cc:dd:ee:99',
                            network_id='12345678-1234-5678-1234567890ab',
                            fixed_ips=[]))

fake_meta_port = dhcp.DictModel(dict(id='12345678-1234-aaaa-1234567890ab',
                                mac_address='aa:bb:cc:dd:ee:ff',
                                network_id='12345678-1234-5678-1234567890ab',
                                device_owner=const.DEVICE_OWNER_ROUTER_INTF,
                                device_id='forzanapoli',
                                fixed_ips=[fake_meta_fixed_ip]))

fake_network = dhcp.NetModel(True, dict(id='12345678-1234-5678-1234567890ab',
                             tenant_id='aaaaaaaa-aaaa-aaaa-aaaaaaaaaaaa',
                             admin_state_up=True,
                             subnets=[fake_subnet1, fake_subnet2],
                             ports=[fake_port1]))

fake_meta_network = dhcp.NetModel(True,
                                  dict(id='12345678-1234-5678-1234567890ab',
                                  tenant_id='aaaaaaaa-aaaa-aaaa-aaaaaaaaaaaa',
                                  admin_state_up=True,
                                  subnets=[fake_meta_subnet],
                                  ports=[fake_meta_port]))

fake_down_network = dhcp.NetModel(True,
                                  dict(id='12345678-dddd-dddd-1234567890ab',
                                  tenant_id='aaaaaaaa-aaaa-aaaa-aaaaaaaaaaaa',
                                  admin_state_up=False,
                                  subnets=[],
                                  ports=[]))


class TestDhcpAgent(base.BaseTestCase):
    def setUp(self):
        super(TestDhcpAgent, self).setUp()
        dhcp_agent.register_options()
        cfg.CONF.set_override('interface_driver',
                              'neutron.agent.linux.interface.NullDriver')
        self.driver_cls_p = mock.patch(
            'neutron.agent.dhcp_agent.importutils.import_class')
        self.driver = mock.Mock(name='driver')
        self.driver.existing_dhcp_networks.return_value = []
        self.driver_cls = self.driver_cls_p.start()
        self.driver_cls.return_value = self.driver
        self.mock_makedirs_p = mock.patch("os.makedirs")
        self.mock_makedirs = self.mock_makedirs_p.start()

    def tearDown(self):
        self.driver_cls_p.stop()
        self.mock_makedirs_p.stop()
        cfg.CONF.reset()
        super(TestDhcpAgent, self).tearDown()

    def test_dhcp_agent_manager(self):
        state_rpc_str = 'neutron.agent.rpc.PluginReportStateAPI'
        with mock.patch.object(DhcpAgentWithStateReport,
                               'sync_state',
                               autospec=True) as mock_sync_state:
            with mock.patch.object(DhcpAgentWithStateReport,
                                   'periodic_resync',
                                   autospec=True) as mock_periodic_resync:
                with mock.patch(state_rpc_str) as state_rpc:
                    with mock.patch.object(sys, 'argv') as sys_argv:
                        sys_argv.return_value = [
                            'dhcp', '--config-file',
                            etcdir('neutron.conf.test')]
                        cfg.CONF.register_opts(dhcp_agent.DhcpAgent.OPTS)
                        config.register_agent_state_opts_helper(cfg.CONF)
                        config.register_root_helper(cfg.CONF)
                        cfg.CONF.register_opts(dhcp.OPTS)
                        cfg.CONF.register_opts(interface.OPTS)
                        cfg.CONF(project='neutron')
                        agent_mgr = DhcpAgentWithStateReport('testhost')
                        eventlet.greenthread.sleep(1)
                        agent_mgr.after_start()
                        mock_sync_state.assert_called_once_with(agent_mgr)
                        mock_periodic_resync.assert_called_once_with(agent_mgr)
                        state_rpc.assert_has_calls(
                            [mock.call(mock.ANY),
                             mock.call().report_state(mock.ANY, mock.ANY,
                                                      mock.ANY)])

    def test_dhcp_agent_main_agent_manager(self):
        logging_str = 'neutron.agent.common.config.setup_logging'
        launcher_str = 'neutron.openstack.common.service.ServiceLauncher'
        with mock.patch(logging_str):
            with mock.patch.object(sys, 'argv') as sys_argv:
                with mock.patch(launcher_str) as launcher:
                    sys_argv.return_value = ['dhcp', '--config-file',
                                             etcdir('neutron.conf.test')]
                    dhcp_agent.main()
                    launcher.assert_has_calls(
                        [mock.call(), mock.call().launch_service(mock.ANY),
                         mock.call().wait()])

    def test_run_completes_single_pass(self):
        with mock.patch(DEVICE_MANAGER):
            dhcp = dhcp_agent.DhcpAgent(HOSTNAME)
            attrs_to_mock = dict(
                [(a, mock.DEFAULT) for a in
                 ['sync_state', 'periodic_resync']])
            with mock.patch.multiple(dhcp, **attrs_to_mock) as mocks:
                dhcp.run()
                mocks['sync_state'].assert_called_once_with()
                mocks['periodic_resync'].assert_called_once_with()

    def test_call_driver(self):
        network = mock.Mock()
        network.id = '1'
        dhcp = dhcp_agent.DhcpAgent(cfg.CONF)
        self.assertTrue(dhcp.call_driver('foo', network))
        self.driver.assert_called_once_with(cfg.CONF,
                                            mock.ANY,
                                            'sudo',
                                            mock.ANY,
                                            mock.ANY)

    def test_call_driver_failure(self):
        network = mock.Mock()
        network.id = '1'
        self.driver.return_value.foo.side_effect = Exception
        with mock.patch.object(dhcp_agent.LOG, 'exception') as log:
            dhcp = dhcp_agent.DhcpAgent(HOSTNAME)
            self.assertIsNone(dhcp.call_driver('foo', network))
            self.driver.assert_called_once_with(cfg.CONF,
                                                mock.ANY,
                                                'sudo',
                                                mock.ANY,
                                                mock.ANY)
            self.assertEqual(log.call_count, 1)
            self.assertTrue(dhcp.needs_resync)

    def _test_sync_state_helper(self, known_networks, active_networks):
        with mock.patch(DHCP_PLUGIN) as plug:
            mock_plugin = mock.Mock()
            mock_plugin.get_active_networks_info.return_value = active_networks
            plug.return_value = mock_plugin

            dhcp = dhcp_agent.DhcpAgent(HOSTNAME)

            attrs_to_mock = dict(
                [(a, mock.DEFAULT) for a in
                 ['refresh_dhcp_helper', 'disable_dhcp_helper', 'cache']])

            with mock.patch.multiple(dhcp, **attrs_to_mock) as mocks:
                mocks['cache'].get_network_ids.return_value = known_networks
                dhcp.sync_state()

                exp_refresh = [
                    mock.call(net_id) for net_id in active_networks]

                diff = set(known_networks) - set(active_networks)
                exp_disable = [mock.call(net_id) for net_id in diff]

                mocks['cache'].assert_has_calls([mock.call.get_network_ids()])
                mocks['refresh_dhcp_helper'].assert_has_called(exp_refresh)
                mocks['disable_dhcp_helper'].assert_has_called(exp_disable)

    def test_sync_state_initial(self):
        self._test_sync_state_helper([], ['a'])

    def test_sync_state_same(self):
        self._test_sync_state_helper(['a'], ['a'])

    def test_sync_state_disabled_net(self):
        self._test_sync_state_helper(['b'], ['a'])

    def test_sync_state_plugin_error(self):
        with mock.patch(DHCP_PLUGIN) as plug:
            mock_plugin = mock.Mock()
            mock_plugin.get_active_networks_info.side_effect = Exception
            plug.return_value = mock_plugin

            with mock.patch.object(dhcp_agent.LOG, 'exception') as log:
                dhcp = dhcp_agent.DhcpAgent(HOSTNAME)
                dhcp.sync_state()

                self.assertTrue(log.called)
                self.assertTrue(dhcp.needs_resync)

    def test_periodic_resync(self):
        dhcp = dhcp_agent.DhcpAgent(HOSTNAME)
        with mock.patch.object(dhcp_agent.eventlet, 'spawn') as spawn:
            dhcp.periodic_resync()
            spawn.assert_called_once_with(dhcp._periodic_resync_helper)

    def test_periodoc_resync_helper(self):
        with mock.patch.object(dhcp_agent.eventlet, 'sleep') as sleep:
            dhcp = dhcp_agent.DhcpAgent(HOSTNAME)
            dhcp.needs_resync = True
            with mock.patch.object(dhcp, 'sync_state') as sync_state:
                sync_state.side_effect = RuntimeError
                with testtools.ExpectedException(RuntimeError):
                    dhcp._periodic_resync_helper()
                sync_state.assert_called_once_with()
                sleep.assert_called_once_with(dhcp.conf.resync_interval)
                self.assertFalse(dhcp.needs_resync)

    def test_populate_cache_on_start_without_active_networks_support(self):
        # emul dhcp driver that doesn't support retrieving of active networks
        self.driver.existing_dhcp_networks.side_effect = NotImplementedError

        with mock.patch.object(dhcp_agent.LOG, 'debug') as log:
            dhcp = dhcp_agent.DhcpAgent(HOSTNAME)

            self.driver.existing_dhcp_networks.assert_called_once_with(
                dhcp.conf,
                cfg.CONF.root_helper
            )

            self.assertFalse(dhcp.cache.get_network_ids())
            self.assertTrue(log.called)

    def test_populate_cache_on_start(self):
        networks = ['aaa', 'bbb']
        self.driver.existing_dhcp_networks.return_value = networks

        dhcp = dhcp_agent.DhcpAgent(HOSTNAME)

        self.driver.existing_dhcp_networks.assert_called_once_with(
            dhcp.conf,
            cfg.CONF.root_helper
        )

        self.assertEqual(set(networks), set(dhcp.cache.get_network_ids()))


class TestLogArgs(base.BaseTestCase):

    def test_log_args_without_log_dir_and_file(self):
        conf_dict = {'debug': True,
                     'verbose': False,
                     'log_dir': None,
                     'log_file': None,
                     'use_syslog': True,
                     'syslog_log_facility': 'LOG_USER'}
        conf = dhcp.DictModel(conf_dict)
        expected_args = ['--debug',
                         '--use-syslog',
                         '--syslog-log-facility=LOG_USER']
        args = config.get_log_args(conf, 'log_file_name')
        self.assertEqual(expected_args, args)

    def test_log_args_without_log_file(self):
        conf_dict = {'debug': True,
                     'verbose': True,
                     'log_dir': '/etc/tests',
                     'log_file': None,
                     'use_syslog': False,
                     'syslog_log_facility': 'LOG_USER'}
        conf = dhcp.DictModel(conf_dict)
        expected_args = ['--debug',
                         '--verbose',
                         '--log-file=log_file_name',
                         '--log-dir=/etc/tests']
        args = config.get_log_args(conf, 'log_file_name')
        self.assertEqual(expected_args, args)

    def test_log_args_with_log_dir_and_file(self):
        conf_dict = {'debug': True,
                     'verbose': False,
                     'log_dir': '/etc/tests',
                     'log_file': 'tests/filelog',
                     'use_syslog': False,
                     'syslog_log_facility': 'LOG_USER'}
        conf = dhcp.DictModel(conf_dict)
        expected_args = ['--debug',
                         '--log-file=log_file_name',
                         '--log-dir=/etc/tests/tests']
        args = config.get_log_args(conf, 'log_file_name')
        self.assertEqual(expected_args, args)

    def test_log_args_without_log_dir(self):
        conf_dict = {'debug': True,
                     'verbose': False,
                     'log_file': 'tests/filelog',
                     'log_dir': None,
                     'use_syslog': False,
                     'syslog_log_facility': 'LOG_USER'}
        conf = dhcp.DictModel(conf_dict)
        expected_args = ['--debug',
                         '--log-file=log_file_name',
                         '--log-dir=tests']
        args = config.get_log_args(conf, 'log_file_name')
        self.assertEqual(expected_args, args)

    def test_log_args_with_filelog_and_syslog(self):
        conf_dict = {'debug': True,
                     'verbose': True,
                     'log_file': 'tests/filelog',
                     'log_dir': '/etc/tests',
                     'use_syslog': True,
                     'syslog_log_facility': 'LOG_USER'}
        conf = dhcp.DictModel(conf_dict)
        expected_args = ['--debug',
                         '--verbose',
                         '--log-file=log_file_name',
                         '--log-dir=/etc/tests/tests']
        args = config.get_log_args(conf, 'log_file_name')
        self.assertEqual(expected_args, args)


class TestDhcpAgentEventHandler(base.BaseTestCase):
    def setUp(self):
        super(TestDhcpAgentEventHandler, self).setUp()
        cfg.CONF.register_opts(dhcp.OPTS)
        cfg.CONF.set_override('interface_driver',
                              'neutron.agent.linux.interface.NullDriver')
        config.register_root_helper(cfg.CONF)
        cfg.CONF.register_opts(dhcp_agent.DhcpAgent.OPTS)

        self.plugin_p = mock.patch(DHCP_PLUGIN)
        plugin_cls = self.plugin_p.start()
        self.plugin = mock.Mock()
        plugin_cls.return_value = self.plugin

        self.cache_p = mock.patch('neutron.agent.dhcp_agent.NetworkCache')
        cache_cls = self.cache_p.start()
        self.cache = mock.Mock()
        cache_cls.return_value = self.cache
        self.mock_makedirs_p = mock.patch("os.makedirs")
        self.mock_makedirs = self.mock_makedirs_p.start()
        self.mock_init_p = mock.patch('neutron.agent.dhcp_agent.'
                                      'DhcpAgent._populate_networks_cache')
        self.mock_init = self.mock_init_p.start()

        with mock.patch.object(dhcp.Dnsmasq,
                               'check_version') as check_v:
            check_v.return_value = dhcp.Dnsmasq.MINIMUM_VERSION
            self.dhcp = dhcp_agent.DhcpAgent(HOSTNAME)
        self.call_driver_p = mock.patch.object(self.dhcp, 'call_driver')

        self.call_driver = self.call_driver_p.start()
        self.external_process_p = mock.patch(
            'neutron.agent.linux.external_process.ProcessManager'
        )
        self.external_process = self.external_process_p.start()

    def tearDown(self):
        self.external_process_p.stop()
        self.call_driver_p.stop()
        self.cache_p.stop()
        self.plugin_p.stop()
        self.mock_makedirs_p.stop()
        self.mock_init_p.stop()
        cfg.CONF.reset()
        super(TestDhcpAgentEventHandler, self).tearDown()

    def _enable_dhcp_helper(self, isolated_metadata=False):
        if isolated_metadata:
            cfg.CONF.set_override('enable_isolated_metadata', True)
        self.plugin.get_network_info.return_value = fake_network
        self.dhcp.enable_dhcp_helper(fake_network.id)
        self.plugin.assert_has_calls(
            [mock.call.get_network_info(fake_network.id)])
        self.call_driver.assert_called_once_with('enable', fake_network)
        self.cache.assert_has_calls([mock.call.put(fake_network)])
        if isolated_metadata:
            self.external_process.assert_has_calls([
                mock.call(
                    cfg.CONF,
                    '12345678-1234-5678-1234567890ab',
                    'sudo',
                    'qdhcp-12345678-1234-5678-1234567890ab'),
                mock.call().enable(mock.ANY)
            ])
        else:
            self.assertFalse(self.external_process.call_count)

    def test_enable_dhcp_helper_enable_isolated_metadata(self):
        self._enable_dhcp_helper(isolated_metadata=True)

    def test_enable_dhcp_helper(self):
        self._enable_dhcp_helper()

    def test_enable_dhcp_helper_down_network(self):
        self.plugin.get_network_info.return_value = fake_down_network
        self.dhcp.enable_dhcp_helper(fake_down_network.id)
        self.plugin.assert_has_calls(
            [mock.call.get_network_info(fake_down_network.id)])
        self.assertFalse(self.call_driver.called)
        self.assertFalse(self.cache.called)
        self.assertFalse(self.external_process.called)

    def test_enable_dhcp_helper_exception_during_rpc(self):
        self.plugin.get_network_info.side_effect = Exception
        with mock.patch.object(dhcp_agent.LOG, 'exception') as log:
            self.dhcp.enable_dhcp_helper(fake_network.id)
            self.plugin.assert_has_calls(
                [mock.call.get_network_info(fake_network.id)])
            self.assertFalse(self.call_driver.called)
            self.assertTrue(log.called)
            self.assertTrue(self.dhcp.needs_resync)
            self.assertFalse(self.cache.called)
            self.assertFalse(self.external_process.called)

    def test_enable_dhcp_helper_driver_failure(self):
        self.plugin.get_network_info.return_value = fake_network
        self.call_driver.return_value = False
        self.dhcp.enable_dhcp_helper(fake_network.id)
        self.plugin.assert_has_calls(
            [mock.call.get_network_info(fake_network.id)])
        self.call_driver.assert_called_once_with('enable', fake_network)
        self.assertFalse(self.cache.called)
        self.assertFalse(self.external_process.called)

    def _disable_dhcp_helper_known_network(self, isolated_metadata=False):
        if isolated_metadata:
            cfg.CONF.set_override('enable_isolated_metadata', True)
        self.cache.get_network_by_id.return_value = fake_network
        self.dhcp.disable_dhcp_helper(fake_network.id)
        self.cache.assert_has_calls(
            [mock.call.get_network_by_id(fake_network.id)])
        self.call_driver.assert_called_once_with('disable', fake_network)
        if isolated_metadata:
            self.external_process.assert_has_calls([
                mock.call(
                    cfg.CONF,
                    '12345678-1234-5678-1234567890ab',
                    'sudo',
                    'qdhcp-12345678-1234-5678-1234567890ab'),
                mock.call().disable()
            ])
        else:
            self.assertFalse(self.external_process.call_count)

    def test_disable_dhcp_helper_known_network_isolated_metadata(self):
        self._disable_dhcp_helper_known_network(isolated_metadata=True)

    def test_disable_dhcp_helper_known_network(self):
        self._disable_dhcp_helper_known_network()

    def test_disable_dhcp_helper_unknown_network(self):
        self.cache.get_network_by_id.return_value = None
        self.dhcp.disable_dhcp_helper('abcdef')
        self.cache.assert_has_calls(
            [mock.call.get_network_by_id('abcdef')])
        self.assertEqual(0, self.call_driver.call_count)
        self.assertFalse(self.external_process.called)

    def _disable_dhcp_helper_driver_failure(self, isolated_metadata=False):
        if isolated_metadata:
            cfg.CONF.set_override('enable_isolated_metadata', True)
        self.cache.get_network_by_id.return_value = fake_network
        self.call_driver.return_value = False
        self.dhcp.disable_dhcp_helper(fake_network.id)
        self.cache.assert_has_calls(
            [mock.call.get_network_by_id(fake_network.id)])
        self.call_driver.assert_called_once_with('disable', fake_network)
        self.cache.assert_has_calls(
            [mock.call.get_network_by_id(fake_network.id)])
        if isolated_metadata:
            self.external_process.assert_has_calls([
                mock.call(
                    cfg.CONF,
                    '12345678-1234-5678-1234567890ab',
                    'sudo',
                    'qdhcp-12345678-1234-5678-1234567890ab'),
                mock.call().disable()
            ])
        else:
            self.assertFalse(self.external_process.call_count)

    def test_disable_dhcp_helper_driver_failure_isolated_metadata(self):
        self._disable_dhcp_helper_driver_failure(isolated_metadata=True)

    def test_disable_dhcp_helper_driver_failure(self):
        self._disable_dhcp_helper_driver_failure()

    def test_enable_isolated_metadata_proxy(self):
        class_path = 'neutron.agent.linux.external_process.ProcessManager'
        with mock.patch(class_path) as ext_process:
            self.dhcp.enable_isolated_metadata_proxy(fake_network)
            ext_process.assert_has_calls([
                mock.call(
                    cfg.CONF,
                    '12345678-1234-5678-1234567890ab',
                    'sudo',
                    'qdhcp-12345678-1234-5678-1234567890ab'),
                mock.call().enable(mock.ANY)
            ])

    def test_disable_isolated_metadata_proxy(self):
        class_path = 'neutron.agent.linux.external_process.ProcessManager'
        with mock.patch(class_path) as ext_process:
            self.dhcp.disable_isolated_metadata_proxy(fake_network)
            ext_process.assert_has_calls([
                mock.call(
                    cfg.CONF,
                    '12345678-1234-5678-1234567890ab',
                    'sudo',
                    'qdhcp-12345678-1234-5678-1234567890ab'),
                mock.call().disable()
            ])

    def test_enable_isolated_metadata_proxy_with_metadata_network(self):
        cfg.CONF.set_override('enable_metadata_network', True)
        cfg.CONF.set_override('debug', True)
        cfg.CONF.set_override('log_file', 'test.log')
        class_path = 'neutron.agent.linux.ip_lib.IPWrapper'
        self.external_process_p.stop()
        # Ensure the mock is restored if this test fail
        try:
            with mock.patch(class_path) as ip_wrapper:
                self.dhcp.enable_isolated_metadata_proxy(fake_meta_network)
                ip_wrapper.assert_has_calls([mock.call(
                    'sudo',
                    'qdhcp-12345678-1234-5678-1234567890ab'),
                    mock.call().netns.execute([
                        'neutron-ns-metadata-proxy',
                        mock.ANY,
                        mock.ANY,
                        '--router_id=forzanapoli',
                        mock.ANY,
                        mock.ANY,
                        '--debug',
                        ('--log-file=neutron-ns-metadata-proxy-%s.log' %
                         fake_meta_network.id)])
                ])
        finally:
            self.external_process_p.start()

    def test_network_create_end(self):
        payload = dict(network=dict(id=fake_network.id))

        with mock.patch.object(self.dhcp, 'enable_dhcp_helper') as enable:
            self.dhcp.network_create_end(None, payload)
            enable.assertCalledOnceWith(fake_network.id)

    def test_network_update_end_admin_state_up(self):
        payload = dict(network=dict(id=fake_network.id, admin_state_up=True))
        with mock.patch.object(self.dhcp, 'enable_dhcp_helper') as enable:
            self.dhcp.network_update_end(None, payload)
            enable.assertCalledOnceWith(fake_network.id)

    def test_network_update_end_admin_state_down(self):
        payload = dict(network=dict(id=fake_network.id, admin_state_up=False))
        with mock.patch.object(self.dhcp, 'disable_dhcp_helper') as disable:
            self.dhcp.network_update_end(None, payload)
            disable.assertCalledOnceWith(fake_network.id)

    def test_network_delete_end(self):
        payload = dict(network_id=fake_network.id)

        with mock.patch.object(self.dhcp, 'disable_dhcp_helper') as disable:
            self.dhcp.network_delete_end(None, payload)
            disable.assertCalledOnceWith(fake_network.id)

    def test_refresh_dhcp_helper_no_dhcp_enabled_networks(self):
        network = dhcp.NetModel(True, dict(id='net-id',
                                tenant_id='aaaaaaaa-aaaa-aaaa-aaaaaaaaaaaa',
                                admin_state_up=True,
                                subnets=[],
                                ports=[]))

        self.cache.get_network_by_id.return_value = network
        self.plugin.get_network_info.return_value = network
        with mock.patch.object(self.dhcp, 'disable_dhcp_helper') as disable:
            self.dhcp.refresh_dhcp_helper(network.id)
            disable.called_once_with_args(network.id)
            self.assertFalse(self.cache.called)
            self.assertFalse(self.call_driver.called)
            self.cache.assert_has_calls(
                [mock.call.get_network_by_id('net-id')])

    def test_refresh_dhcp_helper_exception_during_rpc(self):
        network = dhcp.NetModel(True, dict(id='net-id',
                                tenant_id='aaaaaaaa-aaaa-aaaa-aaaaaaaaaaaa',
                                admin_state_up=True,
                                subnets=[],
                                ports=[]))

        self.cache.get_network_by_id.return_value = network
        self.plugin.get_network_info.side_effect = Exception
        with mock.patch.object(dhcp_agent.LOG, 'exception') as log:
            self.dhcp.refresh_dhcp_helper(network.id)
            self.assertFalse(self.call_driver.called)
            self.cache.assert_has_calls(
                [mock.call.get_network_by_id('net-id')])
            self.assertTrue(log.called)
            self.assertTrue(self.dhcp.needs_resync)

    def test_subnet_update_end(self):
        payload = dict(subnet=dict(network_id=fake_network.id))
        self.cache.get_network_by_id.return_value = fake_network
        self.plugin.get_network_info.return_value = fake_network

        self.dhcp.subnet_update_end(None, payload)

        self.cache.assert_has_calls([mock.call.put(fake_network)])
        self.call_driver.assert_called_once_with('reload_allocations',
                                                 fake_network)

    def test_subnet_update_end_restart(self):
        new_state = dhcp.NetModel(True, dict(id=fake_network.id,
                                  tenant_id=fake_network.tenant_id,
                                  admin_state_up=True,
                                  subnets=[fake_subnet1, fake_subnet3],
                                  ports=[fake_port1]))

        payload = dict(subnet=dict(network_id=fake_network.id))
        self.cache.get_network_by_id.return_value = fake_network
        self.plugin.get_network_info.return_value = new_state

        self.dhcp.subnet_update_end(None, payload)

        self.cache.assert_has_calls([mock.call.put(new_state)])
        self.call_driver.assert_called_once_with('restart',
                                                 new_state)

    def test_subnet_update_end_delete_payload(self):
        prev_state = dhcp.NetModel(True, dict(id=fake_network.id,
                                   tenant_id=fake_network.tenant_id,
                                   admin_state_up=True,
                                   subnets=[fake_subnet1, fake_subnet3],
                                   ports=[fake_port1]))

        payload = dict(subnet_id=fake_subnet1.id)
        self.cache.get_network_by_subnet_id.return_value = prev_state
        self.cache.get_network_by_id.return_value = prev_state
        self.plugin.get_network_info.return_value = fake_network

        self.dhcp.subnet_delete_end(None, payload)

        self.cache.assert_has_calls([
            mock.call.get_network_by_subnet_id(
                'bbbbbbbb-bbbb-bbbb-bbbbbbbbbbbb'),
            mock.call.get_network_by_id('12345678-1234-5678-1234567890ab'),
            mock.call.put(fake_network)])
        self.call_driver.assert_called_once_with('restart',
                                                 fake_network)

    def test_port_update_end(self):
        payload = dict(port=vars(fake_port2))
        self.cache.get_network_by_id.return_value = fake_network
        self.cache.get_port_by_id.return_value = fake_port2
        self.dhcp.port_update_end(None, payload)
        self.cache.assert_has_calls(
            [mock.call.get_network_by_id(fake_port2.network_id),
             mock.call.get_port_by_id(fake_port2.id),
             mock.call.put_port(mock.ANY)])
        self.call_driver.assert_called_once_with('reload_allocations',
                                                 fake_network)

    def test_port_update_change_ip_on_port(self):
        payload = dict(port=vars(fake_port1))
        self.cache.get_network_by_id.return_value = fake_network
        updated_fake_port1 = copy.deepcopy(fake_port1)
        updated_fake_port1.fixed_ips[0].ip_address = '172.9.9.99'
        self.cache.get_port_by_id.return_value = updated_fake_port1
        self.dhcp.port_update_end(None, payload)
        self.cache.assert_has_calls(
            [mock.call.get_network_by_id(fake_port1.network_id),
             mock.call.get_port_by_id(fake_port1.id),
             mock.call.put_port(mock.ANY)])
        self.call_driver.assert_has_calls(
            [mock.call.call_driver('reload_allocations', fake_network),
             mock.call.call_driver(
                 'release_lease',
                 fake_network,
                 mac_address=fake_port1.mac_address,
                 removed_ips=set([updated_fake_port1.fixed_ips[0].ip_address]))
             ])

    def test_port_delete_end(self):
        payload = dict(port_id=fake_port2.id)
        self.cache.get_network_by_id.return_value = fake_network
        self.cache.get_port_by_id.return_value = fake_port2

        self.dhcp.port_delete_end(None, payload)
        removed_ips = [fixed_ip.ip_address
                       for fixed_ip in fake_port2.fixed_ips]
        self.cache.assert_has_calls(
            [mock.call.get_port_by_id(fake_port2.id),
             mock.call.get_network_by_id(fake_network.id),
             mock.call.remove_port(fake_port2)])
        self.call_driver.assert_has_calls(
            [mock.call.call_driver('reload_allocations', fake_network),
             mock.call.call_driver('release_lease',
                                   fake_network,
                                   mac_address=fake_port2.mac_address,
                                   removed_ips=removed_ips)])

    def test_port_delete_end_unknown_port(self):
        payload = dict(port_id='unknown')
        self.cache.get_port_by_id.return_value = None

        self.dhcp.port_delete_end(None, payload)

        self.cache.assert_has_calls([mock.call.get_port_by_id('unknown')])
        self.assertEqual(self.call_driver.call_count, 0)


class TestDhcpPluginApiProxy(base.BaseTestCase):
    def setUp(self):
        super(TestDhcpPluginApiProxy, self).setUp()
        self.proxy = dhcp_agent.DhcpPluginApi('foo', {}, None)
        self.proxy.host = 'foo'

        self.call_p = mock.patch.object(self.proxy, 'call')
        self.call = self.call_p.start()
        self.make_msg_p = mock.patch.object(self.proxy, 'make_msg')
        self.make_msg = self.make_msg_p.start()

    def tearDown(self):
        self.make_msg_p.stop()
        self.call_p.stop()
        super(TestDhcpPluginApiProxy, self).tearDown()

    def test_get_network_info(self):
        self.call.return_value = dict(a=1)
        retval = self.proxy.get_network_info('netid')
        self.assertEqual(retval.a, 1)
        self.assertTrue(self.call.called)
        self.make_msg.assert_called_once_with('get_network_info',
                                              network_id='netid',
                                              host='foo')

    def test_get_dhcp_port(self):
        self.call.return_value = dict(a=1)
        retval = self.proxy.get_dhcp_port('netid', 'devid')
        self.assertEqual(retval.a, 1)
        self.assertTrue(self.call.called)
        self.make_msg.assert_called_once_with('get_dhcp_port',
                                              network_id='netid',
                                              device_id='devid',
                                              host='foo')

    def test_get_active_networks_info(self):
        self.proxy.get_active_networks_info()
        self.make_msg.assert_called_once_with('get_active_networks_info',
                                              host='foo')

    def test_create_dhcp_port(self):
        port_body = (
            {'port':
                {'name': '', 'admin_state_up': True,
                 'network_id': fake_network.id,
                 'tenant_id': fake_network.tenant_id,
                 'fixed_ips': [{'subnet_id': fake_fixed_ip1.subnet_id}],
                 'device_id': mock.ANY}})

        self.proxy.create_dhcp_port(port_body)
        self.make_msg.assert_called_once_with('create_dhcp_port',
                                              port=port_body,
                                              host='foo')

    def test_update_dhcp_port(self):
        port_body = {'port': {'fixed_ips':
                              [{'subnet_id': fake_fixed_ip1.subnet_id}]}}
        self.proxy.update_dhcp_port(fake_port1.id, port_body)
        self.make_msg.assert_called_once_with('update_dhcp_port',
                                              port_id=fake_port1.id,
                                              port=port_body,
                                              host='foo')

    def test_release_dhcp_port(self):
        self.proxy.release_dhcp_port('netid', 'devid')
        self.assertTrue(self.call.called)
        self.make_msg.assert_called_once_with('release_dhcp_port',
                                              network_id='netid',
                                              device_id='devid',
                                              host='foo')

    def test_release_port_fixed_ip(self):
        self.proxy.release_port_fixed_ip('netid', 'devid', 'subid')
        self.assertTrue(self.call.called)
        self.make_msg.assert_called_once_with('release_port_fixed_ip',
                                              network_id='netid',
                                              subnet_id='subid',
                                              device_id='devid',
                                              host='foo')


class TestNetworkCache(base.BaseTestCase):
    def test_put_network(self):
        nc = dhcp_agent.NetworkCache()
        nc.put(fake_network)
        self.assertEqual(nc.cache,
                         {fake_network.id: fake_network})
        self.assertEqual(nc.subnet_lookup,
                         {fake_subnet1.id: fake_network.id,
                          fake_subnet2.id: fake_network.id})
        self.assertEqual(nc.port_lookup,
                         {fake_port1.id: fake_network.id})

    def test_put_network_existing(self):
        prev_network_info = mock.Mock()
        nc = dhcp_agent.NetworkCache()
        with mock.patch.object(nc, 'remove') as remove:
            nc.cache[fake_network.id] = prev_network_info

            nc.put(fake_network)
            remove.assert_called_once_with(prev_network_info)
        self.assertEqual(nc.cache,
                         {fake_network.id: fake_network})
        self.assertEqual(nc.subnet_lookup,
                         {fake_subnet1.id: fake_network.id,
                          fake_subnet2.id: fake_network.id})
        self.assertEqual(nc.port_lookup,
                         {fake_port1.id: fake_network.id})

    def test_remove_network(self):
        nc = dhcp_agent.NetworkCache()
        nc.cache = {fake_network.id: fake_network}
        nc.subnet_lookup = {fake_subnet1.id: fake_network.id,
                            fake_subnet2.id: fake_network.id}
        nc.port_lookup = {fake_port1.id: fake_network.id}
        nc.remove(fake_network)

        self.assertEqual(len(nc.cache), 0)
        self.assertEqual(len(nc.subnet_lookup), 0)
        self.assertEqual(len(nc.port_lookup), 0)

    def test_get_network_by_id(self):
        nc = dhcp_agent.NetworkCache()
        nc.put(fake_network)

        self.assertEqual(nc.get_network_by_id(fake_network.id), fake_network)

    def test_get_network_ids(self):
        nc = dhcp_agent.NetworkCache()
        nc.put(fake_network)

        self.assertEqual(nc.get_network_ids(), [fake_network.id])

    def test_get_network_by_subnet_id(self):
        nc = dhcp_agent.NetworkCache()
        nc.put(fake_network)

        self.assertEqual(nc.get_network_by_subnet_id(fake_subnet1.id),
                         fake_network)

    def test_get_network_by_port_id(self):
        nc = dhcp_agent.NetworkCache()
        nc.put(fake_network)

        self.assertEqual(nc.get_network_by_port_id(fake_port1.id),
                         fake_network)

    def test_put_port(self):
        fake_net = dhcp.NetModel(True,
                                 dict(id='12345678-1234-5678-1234567890ab',
                                 tenant_id='aaaaaaaa-aaaa-aaaa-aaaaaaaaaaaa',
                                 subnets=[fake_subnet1],
                                 ports=[fake_port1]))
        nc = dhcp_agent.NetworkCache()
        nc.put(fake_net)
        nc.put_port(fake_port2)
        self.assertEqual(len(nc.port_lookup), 2)
        self.assertIn(fake_port2, fake_net.ports)

    def test_put_port_existing(self):
        fake_net = dhcp.NetModel(True,
                                 dict(id='12345678-1234-5678-1234567890ab',
                                 tenant_id='aaaaaaaa-aaaa-aaaa-aaaaaaaaaaaa',
                                 subnets=[fake_subnet1],
                                 ports=[fake_port1, fake_port2]))
        nc = dhcp_agent.NetworkCache()
        nc.put(fake_net)
        nc.put_port(fake_port2)

        self.assertEqual(len(nc.port_lookup), 2)
        self.assertIn(fake_port2, fake_net.ports)

    def test_remove_port_existing(self):
        fake_net = dhcp.NetModel(True,
                                 dict(id='12345678-1234-5678-1234567890ab',
                                 tenant_id='aaaaaaaa-aaaa-aaaa-aaaaaaaaaaaa',
                                 subnets=[fake_subnet1],
                                 ports=[fake_port1, fake_port2]))
        nc = dhcp_agent.NetworkCache()
        nc.put(fake_net)
        nc.remove_port(fake_port2)

        self.assertEqual(len(nc.port_lookup), 1)
        self.assertNotIn(fake_port2, fake_net.ports)

    def test_get_port_by_id(self):
        nc = dhcp_agent.NetworkCache()
        nc.put(fake_network)
        self.assertEqual(nc.get_port_by_id(fake_port1.id), fake_port1)


class FakePort1:
    id = 'eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee'


class FakeV4Subnet:
    id = 'dddddddd-dddd-dddd-dddd-dddddddddddd'
    ip_version = 4
    cidr = '192.168.0.0/24'
    gateway_ip = '192.168.0.1'
    enable_dhcp = True


class FakeV4SubnetNoGateway:
    id = 'eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee'
    ip_version = 4
    cidr = '192.168.1.0/24'
    gateway_ip = None
    enable_dhcp = True


class FakeV4Network:
    id = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'
    subnets = [FakeV4Subnet()]
    ports = [FakePort1()]


class FakeV4NetworkNoSubnet:
    id = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'
    subnets = []
    ports = []


class FakeV4NetworkNoGateway:
    id = 'cccccccc-cccc-cccc-cccc-cccccccccccc'
    subnets = [FakeV4SubnetNoGateway()]
    ports = [FakePort1()]


class TestDeviceManager(base.BaseTestCase):
    def setUp(self):
        super(TestDeviceManager, self).setUp()
        cfg.CONF.register_opts(dhcp_agent.DhcpAgent.OPTS)
        cfg.CONF.register_opts(dhcp.OPTS)
        cfg.CONF.set_override('interface_driver',
                              'neutron.agent.linux.interface.NullDriver')
        config.register_root_helper(cfg.CONF)
        cfg.CONF.set_override('use_namespaces', True)
        cfg.CONF.set_override('enable_isolated_metadata', True)

        self.device_exists_p = mock.patch(
            'neutron.agent.linux.ip_lib.device_exists')
        self.device_exists = self.device_exists_p.start()

        self.dvr_cls_p = mock.patch('neutron.agent.linux.interface.NullDriver')
        self.iproute_cls_p = mock.patch('neutron.agent.linux.'
                                        'ip_lib.IpRouteCommand')
        driver_cls = self.dvr_cls_p.start()
        iproute_cls = self.iproute_cls_p.start()
        self.mock_driver = mock.MagicMock()
        self.mock_driver.DEV_NAME_LEN = (
            interface.LinuxInterfaceDriver.DEV_NAME_LEN)
        self.mock_iproute = mock.MagicMock()
        driver_cls.return_value = self.mock_driver
        iproute_cls.return_value = self.mock_iproute

    def tearDown(self):
        self.dvr_cls_p.stop()
        self.device_exists_p.stop()
        self.iproute_cls_p.stop()
        cfg.CONF.reset()
        super(TestDeviceManager, self).tearDown()

    def _test_setup_helper(self, device_exists, reuse_existing=False,
                           net=None, port=None):
        net = net or fake_network
        port = port or fake_port1
        plugin = mock.Mock()
        plugin.create_dhcp_port.return_value = port or fake_port1
        plugin.get_dhcp_port.return_value = port or fake_port1
        self.device_exists.return_value = device_exists
        self.mock_driver.get_device_name.return_value = 'tap12345678-12'

        dh = dhcp.DeviceManager(cfg.CONF, cfg.CONF.root_helper, plugin)
        dh._set_default_route = mock.Mock()
        interface_name = dh.setup(net, reuse_existing)

        self.assertEqual(interface_name, 'tap12345678-12')

        plugin.assert_has_calls([
            mock.call.create_dhcp_port(
                {'port': {'name': '', 'admin_state_up': True,
                          'network_id': net.id, 'tenant_id': net.tenant_id,
                          'fixed_ips':
                          [{'subnet_id': fake_fixed_ip1.subnet_id}],
                          'device_id': mock.ANY}})])

        expected_ips = ['172.9.9.9/24', '169.254.169.254/16']
        expected = [
            mock.call.get_device_name(port),
            mock.call.init_l3(
                'tap12345678-12',
                expected_ips,
                namespace=net.namespace)]

        if not reuse_existing:
            expected.insert(1,
                            mock.call.plug(net.id,
                                           port.id,
                                           'tap12345678-12',
                                           'aa:bb:cc:dd:ee:ff',
                                           namespace=net.namespace))
        self.mock_driver.assert_has_calls(expected)

        dh._set_default_route.assert_called_once_with(net)

    def test_setup(self):
        cfg.CONF.set_override('enable_metadata_network', False)
        self._test_setup_helper(False)
        cfg.CONF.set_override('enable_metadata_network', True)
        self._test_setup_helper(False)

    def test_setup_device_exists(self):
        with testtools.ExpectedException(exceptions.PreexistingDeviceFailure):
            self._test_setup_helper(True)

    def test_setup_device_exists_reuse(self):
        self._test_setup_helper(True, True)

    def test_create_dhcp_port_create_new(self):
        plugin = mock.Mock()
        dh = dhcp.DeviceManager(cfg.CONF, cfg.CONF.root_helper, plugin)
        plugin.create_dhcp_port.return_value = fake_network.ports[0]
        dh.setup_dhcp_port(fake_network)
        plugin.assert_has_calls([
            mock.call.create_dhcp_port(
                {'port': {'name': '', 'admin_state_up': True,
                          'network_id':
                          fake_network.id, 'tenant_id': fake_network.tenant_id,
                          'fixed_ips':
                          [{'subnet_id': fake_fixed_ip1.subnet_id}],
                          'device_id': mock.ANY}})])

    def test_create_dhcp_port_update_add_subnet(self):
        plugin = mock.Mock()
        dh = dhcp.DeviceManager(cfg.CONF, cfg.CONF.root_helper, plugin)
        fake_network_copy = copy.deepcopy(fake_network)
        fake_network_copy.ports[0].device_id = dh.get_device_id(fake_network)
        fake_network_copy.subnets[1].enable_dhcp = True
        plugin.update_dhcp_port.return_value = fake_network.ports[0]
        dh.setup_dhcp_port(fake_network_copy)
        port_body = {'port': {
                     'fixed_ips': [{'subnet_id': fake_fixed_ip1.subnet_id,
                                    'ip_address': fake_fixed_ip1.ip_address},
                                   {'subnet_id': fake_subnet2.id}]}}

        plugin.assert_has_calls([
            mock.call.update_dhcp_port(fake_network_copy.ports[0].id,
                                       port_body)])

    def test_create_dhcp_port_no_update_or_create(self):
        plugin = mock.Mock()
        dh = dhcp.DeviceManager(cfg.CONF, cfg.CONF.root_helper, plugin)
        fake_network_copy = copy.deepcopy(fake_network)
        fake_network_copy.ports[0].device_id = dh.get_device_id(fake_network)
        dh.setup_dhcp_port(fake_network_copy)
        self.assertFalse(plugin.setup_dhcp_port.called)
        self.assertFalse(plugin.update_dhcp_port.called)

    def test_destroy(self):
        fake_net = dhcp.NetModel(True,
                                 dict(id='12345678-1234-5678-1234567890ab',
                                 tenant_id='aaaaaaaa-aaaa-aaaa-aaaaaaaaaaaa'))

        fake_port = dhcp.DictModel(dict(id='12345678-1234-aaaa-1234567890ab',
                                   mac_address='aa:bb:cc:dd:ee:ff'))

        with mock.patch('neutron.agent.linux.interface.NullDriver') as dvr_cls:
            mock_driver = mock.MagicMock()
            mock_driver.get_device_name.return_value = 'tap12345678-12'
            dvr_cls.return_value = mock_driver

            plugin = mock.Mock()
            plugin.get_dhcp_port.return_value = fake_port

            dh = dhcp.DeviceManager(cfg.CONF, cfg.CONF.root_helper, plugin)
            dh.destroy(fake_net, 'tap12345678-12')

            dvr_cls.assert_called_once_with(cfg.CONF)
            mock_driver.assert_has_calls(
                [mock.call.unplug('tap12345678-12',
                                  namespace='qdhcp-' + fake_net.id)])
            plugin.assert_has_calls(
                [mock.call.release_dhcp_port(fake_net.id, mock.ANY)])

    def test_get_interface_name(self):
        fake_net = dhcp.NetModel(True,
                                 dict(id='12345678-1234-5678-1234567890ab',
                                 tenant_id='aaaaaaaa-aaaa-aaaa-aaaaaaaaaaaa'))

        fake_port = dhcp.DictModel(dict(id='12345678-1234-aaaa-1234567890ab',
                                   mac_address='aa:bb:cc:dd:ee:ff'))

        with mock.patch('neutron.agent.linux.interface.NullDriver') as dvr_cls:
            mock_driver = mock.MagicMock()
            mock_driver.get_device_name.return_value = 'tap12345678-12'
            dvr_cls.return_value = mock_driver

            plugin = mock.Mock()
            plugin.get_dhcp_port.return_value = fake_port

            dh = dhcp.DeviceManager(cfg.CONF, cfg.CONF.root_helper, plugin)
            dh.get_interface_name(fake_net, fake_port)

            dvr_cls.assert_called_once_with(cfg.CONF)
            mock_driver.assert_has_calls(
                [mock.call.get_device_name(fake_port)])

            self.assertEqual(len(plugin.mock_calls), 0)

    def test_get_device_id(self):
        fake_net = dhcp.NetModel(True,
                                 dict(id='12345678-1234-5678-1234567890ab',
                                 tenant_id='aaaaaaaa-aaaa-aaaa-aaaaaaaaaaaa'))
        expected = ('dhcp1ae5f96c-c527-5079-82ea-371a01645457-12345678-1234-'
                    '5678-1234567890ab')

        with mock.patch('socket.gethostbyname') as get_host:
            with mock.patch('uuid.uuid5') as uuid5:
                uuid5.return_value = '1ae5f96c-c527-5079-82ea-371a01645457'
                get_host.return_value = 'localhost'

                dh = dhcp.DeviceManager(cfg.CONF, cfg.CONF.root_helper, None)
                uuid5.called_once_with(uuid.NAMESPACE_DNS, 'localhost')
                self.assertEqual(dh.get_device_id(fake_net), expected)

    def _get_device_manager_with_mock_device(self, conf, device):
            dh = dhcp.DeviceManager(conf, cfg.CONF.root_helper, None)
            dh._get_device = mock.Mock(return_value=device)
            return dh

    def test_update(self):
        # Try with namespaces and no metadata network
        cfg.CONF.set_override('use_namespaces', True)
        cfg.CONF.set_override('enable_metadata_network', False)
        dh = dhcp.DeviceManager(cfg.CONF, cfg.CONF.root_helper, None)
        dh._set_default_route = mock.Mock()

        dh.update(True)

        dh._set_default_route.assert_called_once_with(True)

        # No namespaces, shouldn't set default route.
        cfg.CONF.set_override('use_namespaces', False)
        cfg.CONF.set_override('enable_metadata_network', False)
        dh = dhcp.DeviceManager(cfg.CONF, cfg.CONF.root_helper, None)
        dh._set_default_route = mock.Mock()

        dh.update(FakeV4Network())

        self.assertFalse(dh._set_default_route.called)

        # Meta data network enabled, don't interfere with its gateway.
        cfg.CONF.set_override('use_namespaces', True)
        cfg.CONF.set_override('enable_metadata_network', True)
        dh = dhcp.DeviceManager(cfg.CONF, cfg.CONF.root_helper, None)
        dh._set_default_route = mock.Mock()

        dh.update(FakeV4Network())

        self.assertTrue(dh._set_default_route.called)

        # For completeness
        cfg.CONF.set_override('use_namespaces', False)
        cfg.CONF.set_override('enable_metadata_network', True)
        dh = dhcp.DeviceManager(cfg.CONF, cfg.CONF.root_helper, None)
        dh._set_default_route = mock.Mock()

        dh.update(FakeV4Network())

        self.assertFalse(dh._set_default_route.called)

    def test_set_default_route(self):
        device = mock.Mock()
        device.route.get_gateway.return_value = None

        # Basic one subnet with gateway.
        dh = self._get_device_manager_with_mock_device(cfg.CONF, device)
        network = FakeV4Network()

        dh._set_default_route(network)

        device.route.get_gateway.assert_called_once()
        self.assertFalse(device.route.delete_gateway.called)
        device.route.add_gateway.assert_called_once_with('192.168.0.1')

    def test_set_default_route_no_subnet(self):
        device = mock.Mock()
        device.route.get_gateway.return_value = None

        # Try a namespace but no subnet.
        dh = self._get_device_manager_with_mock_device(cfg.CONF, device)
        network = FakeV4NetworkNoSubnet()

        dh._set_default_route(network)

        device.route.get_gateway.assert_called_once()
        self.assertFalse(device.route.delete_gateway.called)
        self.assertFalse(device.route.add_gateway.called)

    def test_set_default_route_no_subnet_delete_gateway(self):
        device = mock.Mock()
        device.route.get_gateway.return_value = dict(gateway='192.168.0.1')

        # Try a namespace but no subnet where a gateway needs to be deleted.
        dh = self._get_device_manager_with_mock_device(cfg.CONF, device)
        network = FakeV4NetworkNoSubnet()

        dh._set_default_route(network)

        device.route.get_gateway.assert_called_once()
        device.route.delete_gateway.assert_called_once_with('192.168.0.1')
        self.assertFalse(device.route.add_gateway.called)

    def test_set_default_route_no_gateway(self):
        device = mock.Mock()
        device.route.get_gateway.return_value = dict(gateway='192.168.0.1')

        # Try a subnet with no gateway
        dh = self._get_device_manager_with_mock_device(cfg.CONF, device)
        network = FakeV4NetworkNoGateway()

        dh._set_default_route(network)

        device.route.get_gateway.assert_called_once()
        device.route.delete_gateway.assert_called_once_with('192.168.0.1')
        self.assertFalse(device.route.add_gateway.called)

    def test_set_default_route_do_nothing(self):
        device = mock.Mock()
        device.route.get_gateway.return_value = dict(gateway='192.168.0.1')

        # Try a subnet where the gateway doesn't change.  Should do nothing.
        dh = self._get_device_manager_with_mock_device(cfg.CONF, device)
        network = FakeV4Network()

        dh._set_default_route(network)

        device.route.get_gateway.assert_called_once()
        self.assertFalse(device.route.delete_gateway.called)
        self.assertFalse(device.route.add_gateway.called)

    def test_set_default_route_change_gateway(self):
        device = mock.Mock()
        device.route.get_gateway.return_value = dict(gateway='192.168.0.2')

        # Try a subnet with a gateway this is different than the current.
        dh = self._get_device_manager_with_mock_device(cfg.CONF, device)
        network = FakeV4Network()

        dh._set_default_route(network)

        device.route.get_gateway.assert_called_once()
        self.assertFalse(device.route.delete_gateway.called)
        device.route.add_gateway.assert_called_once_with('192.168.0.1')

    def test_set_default_route_two_subnets(self):
        device = mock.Mock()
        device.route.get_gateway.return_value = None

        # Try two subnets.  Should set gateway from the first.
        dh = self._get_device_manager_with_mock_device(cfg.CONF, device)
        network = FakeV4Network()
        subnet2 = FakeV4Subnet()
        subnet2.gateway_ip = '192.168.1.1'
        network.subnets = [subnet2, FakeV4Subnet()]

        dh._set_default_route(network)

        device.route.get_gateway.assert_called_once()
        self.assertFalse(device.route.delete_gateway.called)
        device.route.add_gateway.assert_called_once_with('192.168.1.1')


class TestDictModel(base.BaseTestCase):
    def test_basic_dict(self):
        d = dict(a=1, b=2)

        m = dhcp.DictModel(d)
        self.assertEqual(m.a, 1)
        self.assertEqual(m.b, 2)

    def test_dict_has_sub_dict(self):
        d = dict(a=dict(b=2))
        m = dhcp.DictModel(d)
        self.assertEqual(m.a.b, 2)

    def test_dict_contains_list(self):
        d = dict(a=[1, 2])

        m = dhcp.DictModel(d)
        self.assertEqual(m.a, [1, 2])

    def test_dict_contains_list_of_dicts(self):
        d = dict(a=[dict(b=2), dict(c=3)])

        m = dhcp.DictModel(d)
        self.assertEqual(m.a[0].b, 2)
        self.assertEqual(m.a[1].c, 3)


class TestNetModel(base.BaseTestCase):
    def test_ns_name(self):
        network = dhcp.NetModel(True, {'id': 'foo'})
        self.assertEqual(network.namespace, 'qdhcp-foo')

    def test_ns_name_false_namespace(self):
        network = dhcp.NetModel(False, {'id': 'foo'})
        self.assertIsNone(network.namespace)

    def test_ns_name_none_namespace(self):
        network = dhcp.NetModel(None, {'id': 'foo'})
        self.assertIsNone(network.namespace)
