From de1d7d953772e3667c75caacd8a44f7026f88228 Mon Sep 17 00:00:00 2001 From: xchenum Date: Wed, 25 Jul 2012 20:58:52 -0400 Subject: [PATCH] Implementation of bp per-net-dhcp-enable Change-Id: I81c2e6adb02921e8b80f8181a730b1cba9ffa649 --- quantum/agent/dhcp_agent.py | 11 +- quantum/agent/linux/dhcp.py | 29 ++++- quantum/api/v2/attributes.py | 5 + quantum/db/db_base_plugin_v2.py | 6 +- quantum/db/models_v2.py | 2 + quantum/tests/unit/test_api_v2.py | 2 +- quantum/tests/unit/test_db_plugin.py | 26 ++-- quantum/tests/unit/test_dhcp_agent.py | 164 ++++++++++++++++++++++++-- quantum/tests/unit/test_linux_dhcp.py | 25 ++++ 9 files changed, 245 insertions(+), 25 deletions(-) diff --git a/quantum/agent/dhcp_agent.py b/quantum/agent/dhcp_agent.py index 3e4c4ed56..8ab0c9254 100644 --- a/quantum/agent/dhcp_agent.py +++ b/quantum/agent/dhcp_agent.py @@ -104,12 +104,21 @@ class DhcpAgent(object): subnets = {} subnet_hashes = set() + + network_admin_up = {} + for network in self.db.networks.all(): + network_admin_up[network.id] = network.admin_state_up + for subnet in self.db.subnets.all(): + if (not subnet.enable_dhcp or + not network_admin_up[subnet.network_id]): + continue subnet_hashes.add((hash(str(subnet)), subnet.network_id)) subnets[subnet.id] = subnet.network_id ipalloc_hashes = set([(hash(str(a)), subnets[a.subnet_id]) - for a in self.db.ipallocations.all()]) + for a in self.db.ipallocations.all() + if a.subnet_id in subnets]) networks = set(subnets.itervalues()) diff --git a/quantum/agent/linux/dhcp.py b/quantum/agent/linux/dhcp.py index ba5656370..5ed015867 100644 --- a/quantum/agent/linux/dhcp.py +++ b/quantum/agent/linux/dhcp.py @@ -90,14 +90,20 @@ class DhcpBase(object): class DhcpLocalProcess(DhcpBase): PORTS = [] + def _enable_dhcp(self): + """check if there is a subnet within the network with dhcp enabled.""" + for subnet in self.network.subnets: + if subnet.enable_dhcp: + return True + return False + def enable(self): """Enables DHCP for this network by spawning a local process.""" if self.active: self.reload_allocations() - return - - self.device_delegate.setup(self.network, reuse_existing=True) - self.spawn_process() + elif self._enable_dhcp(): + self.device_delegate.setup(self.network, reuse_existing=True) + self.spawn_process() def disable(self): """Disable DHCP for this network by killing the local process.""" @@ -193,6 +199,9 @@ class Dnsmasq(DhcpLocalProcess): ] for i, subnet in enumerate(self.network.subnets): + # if a subnet is specified to have dhcp disabled + if not subnet.enable_dhcp: + continue if subnet.ip_version == 4: mode = 'static' else: @@ -213,6 +222,13 @@ class Dnsmasq(DhcpLocalProcess): utils.execute(cmd, self.root_helper) def reload_allocations(self): + """If all subnets turn off dhcp, kill the process.""" + if not self._enable_dhcp(): + self.disable() + LOG.debug(_('Killing dhcpmasq for network since all subnets have \ + turned off DHCP: %s') % self.network.id) + return + """Rebuilds the dnsmasq config and signal the dnsmasq to reload.""" self._output_hosts_file() self._output_opts_file() @@ -240,9 +256,12 @@ class Dnsmasq(DhcpLocalProcess): # TODO (mark): add support for nameservers options = [] for i, subnet in enumerate(self.network.subnets): - if subnet.ip_version == 6: + if not subnet.enable_dhcp: + continue + elif subnet.ip_version == 6: continue else: + #NOTE(xchenum) need to handle no gw case options.append((self._TAG_PREFIX % i, 'option', 'router', diff --git a/quantum/api/v2/attributes.py b/quantum/api/v2/attributes.py index 05fe29250..980231879 100644 --- a/quantum/api/v2/attributes.py +++ b/quantum/api/v2/attributes.py @@ -228,5 +228,10 @@ RESOURCE_ATTRIBUTE_MAP = { 'tenant_id': {'allow_post': True, 'allow_put': False, 'required_by_policy': True, 'is_visible': True}, + 'enable_dhcp': {'allow_post': True, 'allow_put': True, + 'default': True, + 'convert_to': convert_to_boolean, + 'validate': {'type:boolean': None}, + 'is_visible': True}, } } diff --git a/quantum/db/db_base_plugin_v2.py b/quantum/db/db_base_plugin_v2.py index 7b4d10e4c..d07e5d0d0 100644 --- a/quantum/db/db_base_plugin_v2.py +++ b/quantum/db/db_base_plugin_v2.py @@ -625,7 +625,8 @@ class QuantumDbPluginV2(quantum_plugin_base_v2.QuantumPluginBaseV2): 'allocation_pools': [{'start': pool['first_ip'], 'end': pool['last_ip']} for pool in subnet['allocation_pools']], - 'gateway_ip': subnet['gateway_ip']} + 'gateway_ip': subnet['gateway_ip'], + 'enable_dhcp': subnet['enable_dhcp']} return self._fields(res, fields) def _make_port_dict(self, port, fields=None): @@ -702,7 +703,8 @@ class QuantumDbPluginV2(quantum_plugin_base_v2.QuantumPluginBaseV2): network_id=s['network_id'], ip_version=s['ip_version'], cidr=s['cidr'], - gateway_ip=s['gateway_ip']) + gateway_ip=s['gateway_ip'], + enable_dhcp=s['enable_dhcp']) context.session.add(subnet) pools = self._allocate_pools_for_subnet(context, s) for pool in pools: diff --git a/quantum/db/models_v2.py b/quantum/db/models_v2.py index ec7a55b8e..a076a956f 100644 --- a/quantum/db/models_v2.py +++ b/quantum/db/models_v2.py @@ -112,6 +112,8 @@ class Subnet(model_base.BASEV2, HasId, HasTenant): allocation_pools = orm.relationship(IPAllocationPool, backref='subnet', lazy="dynamic") + enable_dhcp = sa.Column(sa.Boolean()) + #TODO(danwent): # - dns_namservers # - additional_routes diff --git a/quantum/tests/unit/test_api_v2.py b/quantum/tests/unit/test_api_v2.py index 0f6105eeb..5aae422df 100644 --- a/quantum/tests/unit/test_api_v2.py +++ b/quantum/tests/unit/test_api_v2.py @@ -752,7 +752,7 @@ class V2Views(unittest.TestCase): def test_subnet(self): keys = ('id', 'network_id', 'tenant_id', 'gateway_ip', - 'ip_version', 'cidr') + 'ip_version', 'cidr', 'enable_dhcp') self._view(keys, 'subnets', 'subnet') diff --git a/quantum/tests/unit/test_db_plugin.py b/quantum/tests/unit/test_db_plugin.py index 45f9fc243..35e46f42d 100644 --- a/quantum/tests/unit/test_db_plugin.py +++ b/quantum/tests/unit/test_db_plugin.py @@ -137,12 +137,13 @@ class QuantumDbPluginV2TestCase(unittest2.TestCase): return network_req.get_response(self.api) def _create_subnet(self, fmt, tenant_id, net_id, gateway_ip, cidr, - allocation_pools=None, ip_version=4): + allocation_pools=None, ip_version=4, enable_dhcp=True): data = {'subnet': {'tenant_id': tenant_id, 'network_id': net_id, 'cidr': cidr, 'ip_version': ip_version, - 'tenant_id': self._tenant_id}} + 'tenant_id': self._tenant_id, + 'enable_dhcp': enable_dhcp}} if gateway_ip: data['subnet']['gateway_ip'] = gateway_ip if allocation_pools: @@ -165,14 +166,15 @@ class QuantumDbPluginV2TestCase(unittest2.TestCase): return port_req.get_response(self.api) def _make_subnet(self, fmt, network, gateway, cidr, - allocation_pools=None, ip_version=4): + allocation_pools=None, ip_version=4, enable_dhcp=True): res = self._create_subnet(fmt, network['network']['tenant_id'], network['network']['id'], gateway, cidr, allocation_pools=allocation_pools, - ip_version=ip_version) + ip_version=ip_version, + enable_dhcp=enable_dhcp) # Things can go wrong - raise HTTP exc with res code only # so it can be caught by unit tests if res.status_int >= 400: @@ -200,7 +202,8 @@ class QuantumDbPluginV2TestCase(unittest2.TestCase): cidr='10.0.0.0/24', fmt='json', ip_version=4, - allocation_pools=None): + allocation_pools=None, + enable_dhcp=True): # TODO(anyone) DRY this # NOTE(salvatore-orlando): we can pass the network object # to gen function anyway, and then avoid the repetition @@ -211,7 +214,8 @@ class QuantumDbPluginV2TestCase(unittest2.TestCase): gateway_ip, cidr, allocation_pools, - ip_version) + ip_version, + enable_dhcp) yield subnet self._delete('subnets', subnet['subnet']['id']) else: @@ -220,7 +224,8 @@ class QuantumDbPluginV2TestCase(unittest2.TestCase): gateway_ip, cidr, allocation_pools, - ip_version) + ip_version, + enable_dhcp) yield subnet self._delete('subnets', subnet['subnet']['id']) @@ -876,6 +881,7 @@ class TestSubnetsV2(QuantumDbPluginV2TestCase): keys = kwargs.copy() keys.setdefault('cidr', '10.0.0.0/24') keys.setdefault('ip_version', 4) + keys.setdefault('enable_dhcp', True) with self.subnet(network=network, **keys) as subnet: # verify the response has each key with the correct value for k in keys: @@ -974,10 +980,12 @@ class TestSubnetsV2(QuantumDbPluginV2TestCase): cidr = '10.0.0.0/24' allocation_pools = [{'start': '10.0.0.2', 'end': '10.0.0.254'}] + enable_dhcp = True subnet = self._test_create_subnet() # verify cidr & gw have been correctly generated self.assertEquals(subnet['subnet']['cidr'], cidr) self.assertEquals(subnet['subnet']['gateway_ip'], gateway) + self.assertEquals(subnet['subnet']['enable_dhcp'], enable_dhcp) self.assertEquals(subnet['subnet']['allocation_pools'], allocation_pools) @@ -1021,6 +1029,10 @@ class TestSubnetsV2(QuantumDbPluginV2TestCase): cidr=cidr, allocation_pools=allocation_pools) + def test_create_subnet_with_dhcp_disabled(self): + enable_dhcp = False + self._test_create_subnet(enable_dhcp=enable_dhcp) + def test_create_subnet_gateway_in_allocation_pool_returns_409(self): gateway_ip = '10.0.0.50' cidr = '10.0.0.0/24' diff --git a/quantum/tests/unit/test_dhcp_agent.py b/quantum/tests/unit/test_dhcp_agent.py index cac9c6510..0eeb7eb75 100644 --- a/quantum/tests/unit/test_dhcp_agent.py +++ b/quantum/tests/unit/test_dhcp_agent.py @@ -70,6 +70,7 @@ class TestDhcpAgent(unittest.TestCase): self.dhcp.daemon_loop() def test_daemon_loop_completes_single_pass(self): + self.dhcp._network_dhcp_enable = mock.Mock(return_value=True) with mock.patch.object(self.dhcp, 'get_network_state_delta') as state: with mock.patch.object(self.dhcp, 'call_driver') as call_driver: with mock.patch('quantum.agent.dhcp_agent.time') as time: @@ -84,18 +85,75 @@ class TestDhcpAgent(unittest.TestCase): mock.call('reload_allocations', 'updated_net'), mock.call('disable', 'deleted_net')]) - def test_state_builder(self): - fake_subnet = [ - FakeModel(1, network_id=1), - FakeModel(2, network_id=2), + def test_state_builder_network_admin_down(self): + fake_network1 = FakeModel(1, admin_state_up=True) + fake_network2 = FakeModel(2, admin_state_up=False) + + fake_subnet1 = FakeModel(1, network_id=1, enable_dhcp=True) + fake_subnet2 = FakeModel(2, network_id=2, enable_dhcp=True) + fake_subnet3 = FakeModel(3, network_id=2, enable_dhcp=True) + + fake_network1.subnets = [fake_subnet1] + fake_network2.subnets = [fake_subnet2, fake_subnet3] + + fake_subnet1.network = fake_network1 + fake_subnet2.network = fake_network2 + fake_subnet3.network = fake_network2 + + fake_allocation = [ + FakeModel(2, subnet_id=1), + FakeModel(3, subnet_id=2) ] + fake_subnets = [fake_subnet1, fake_subnet2, fake_subnet3] + fake_networks = [fake_network1, fake_network2] + + db = mock.Mock() + db.subnets.all = mock.Mock(return_value=fake_subnets) + db.networks.all = mock.Mock(return_value=fake_networks) + db.ipallocations.all = mock.Mock(return_value=fake_allocation) + self.dhcp.db = db + state = self.dhcp._state_builder() + + self.assertEquals(state.networks, set([1])) + + expected_subnets = set([ + (hash(str(fake_subnets[0])), 1), + ]) + self.assertEquals(state.subnet_hashes, expected_subnets) + + expected_ipalloc = set([ + (hash(str(fake_allocation[0])), 1), + ]) + self.assertEquals(state.ipalloc_hashes, expected_ipalloc) + + def test_state_builder_network_dhcp_partial_disable(self): + fake_network1 = FakeModel(1, admin_state_up=True) + fake_network2 = FakeModel(2, admin_state_up=True) + + fake_subnet1 = FakeModel(1, network_id=1, enable_dhcp=True) + fake_subnet2 = FakeModel(2, network_id=2, enable_dhcp=False) + fake_subnet3 = FakeModel(3, network_id=2, enable_dhcp=True) + + fake_network1.subnets = [fake_subnet1] + fake_network2.subnets = [fake_subnet2, fake_subnet3] + + fake_subnet1.network = fake_network1 + fake_subnet2.network = fake_network2 + fake_subnet3.network = fake_network2 + fake_allocation = [ - FakeModel(2, subnet_id=1) + FakeModel(2, subnet_id=1), + FakeModel(3, subnet_id=2), + FakeModel(4, subnet_id=3), ] + fake_subnets = [fake_subnet1, fake_subnet2, fake_subnet3] + fake_networks = [fake_network1, fake_network2] + db = mock.Mock() - db.subnets.all = mock.Mock(return_value=fake_subnet) + db.subnets.all = mock.Mock(return_value=fake_subnets) + db.networks.all = mock.Mock(return_value=fake_networks) db.ipallocations.all = mock.Mock(return_value=fake_allocation) self.dhcp.db = db state = self.dhcp._state_builder() @@ -103,8 +161,95 @@ class TestDhcpAgent(unittest.TestCase): self.assertEquals(state.networks, set([1, 2])) expected_subnets = set([ - (hash(str(fake_subnet[0])), 1), - (hash(str(fake_subnet[1])), 2) + (hash(str(fake_subnets[0])), 1), + (hash(str(fake_subnets[2])), 2), + ]) + self.assertEquals(state.subnet_hashes, expected_subnets) + + expected_ipalloc = set([ + (hash(str(fake_allocation[0])), 1), + (hash(str(fake_allocation[2])), 2), + ]) + self.assertEquals(state.ipalloc_hashes, expected_ipalloc) + + def test_state_builder_network_dhcp_all_disable(self): + fake_network1 = FakeModel(1, admin_state_up=True) + fake_network2 = FakeModel(2, admin_state_up=True) + + fake_subnet1 = FakeModel(1, network_id=1, enable_dhcp=True) + fake_subnet2 = FakeModel(2, network_id=2, enable_dhcp=False) + fake_subnet3 = FakeModel(3, network_id=2, enable_dhcp=False) + + fake_network1.subnets = [fake_subnet1] + fake_network2.subnets = [fake_subnet2, fake_subnet3] + + fake_subnet1.network = fake_network1 + fake_subnet2.network = fake_network2 + fake_subnet3.network = fake_network2 + + fake_allocation = [ + FakeModel(2, subnet_id=1), + FakeModel(3, subnet_id=2), + FakeModel(4, subnet_id=3), + ] + + fake_subnets = [fake_subnet1, fake_subnet2, fake_subnet3] + fake_networks = [fake_network1, fake_network2] + + db = mock.Mock() + db.subnets.all = mock.Mock(return_value=fake_subnets) + db.networks.all = mock.Mock(return_value=fake_networks) + db.ipallocations.all = mock.Mock(return_value=fake_allocation) + self.dhcp.db = db + state = self.dhcp._state_builder() + + self.assertEquals(state.networks, set([1])) + + expected_subnets = set([ + (hash(str(fake_subnets[0])), 1) + ]) + self.assertEquals(state.subnet_hashes, expected_subnets) + + expected_ipalloc = set([ + (hash(str(fake_allocation[0])), 1) + ]) + self.assertEquals(state.ipalloc_hashes, expected_ipalloc) + + def test_state_builder_mixed(self): + fake_network1 = FakeModel(1, admin_state_up=True) + fake_network2 = FakeModel(2, admin_state_up=True) + fake_network3 = FakeModel(3, admin_state_up=False) + + fake_subnet1 = FakeModel(1, network_id=1, enable_dhcp=True) + fake_subnet2 = FakeModel(2, network_id=2, enable_dhcp=False) + fake_subnet3 = FakeModel(3, network_id=3, enable_dhcp=True) + + fake_network1.subnets = [fake_subnet1] + fake_network2.subnets = [fake_subnet2] + fake_network3.subnets = [fake_subnet3] + + fake_subnet1.network = fake_network1 + fake_subnet2.network = fake_network2 + fake_subnet3.network = fake_network3 + + fake_allocation = [ + FakeModel(2, subnet_id=1) + ] + + fake_subnets = [fake_subnet1, fake_subnet2, fake_subnet3] + fake_networks = [fake_network1, fake_network2, fake_network3] + + db = mock.Mock() + db.subnets.all = mock.Mock(return_value=fake_subnets) + db.networks.all = mock.Mock(return_value=fake_networks) + db.ipallocations.all = mock.Mock(return_value=fake_allocation) + self.dhcp.db = db + state = self.dhcp._state_builder() + + self.assertEquals(state.networks, set([1])) + + expected_subnets = set([ + (hash(str(fake_subnets[0])), 1), ]) self.assertEquals(state.subnet_hashes, expected_subnets) @@ -120,7 +265,8 @@ class TestDhcpAgent(unittest.TestCase): return self.dhcp.get_network_state_delta() def test_get_network_state_fresh(self): - new_state = dhcp_agent.State(set([1]), set([(3, 1)]), set([(11, 1)])) + new_state = dhcp_agent.State(set([1]), set([(3, 1)]), + set([(11, 1)])) delta = self._network_state_helper(self.dhcp.prev_state, new_state) self.assertEqual(delta, diff --git a/quantum/tests/unit/test_linux_dhcp.py b/quantum/tests/unit/test_linux_dhcp.py index 37b0dbb06..e20e3a392 100644 --- a/quantum/tests/unit/test_linux_dhcp.py +++ b/quantum/tests/unit/test_linux_dhcp.py @@ -58,6 +58,7 @@ class FakeV4Subnet: ip_version = 4 cidr = '192.168.0.0/24' gateway_ip = '192.168.0.1' + enable_dhcp = True class FakeV6Subnet: @@ -65,6 +66,15 @@ class FakeV6Subnet: ip_version = 6 cidr = 'fdca:3ba5:a17a:4ba3::/64' gateway_ip = 'fdca:3ba5:a17a:4ba3::1' + enable_dhcp = True + + +class FakeV4SubnetNoDHCP: + id = 'eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee' + ip_version = 4 + cidr = '192.168.1.0/24' + gateway_ip = '192.168.1.1' + enable_dhcp = False class FakeV4Network: @@ -85,6 +95,12 @@ class FakeDualNetwork: ports = [FakePort1(), FakePort2(), FakePort3()] +class FakeDualNetworkSingleDHCP: + id = 'cccccccc-cccc-cccc-cccc-cccccccccccc' + subnets = [FakeV4Subnet(), FakeV4SubnetNoDHCP()] + ports = [FakePort1(), FakePort2(), FakePort3()] + + class TestDhcpBase(unittest.TestCase): def test_base_abc_error(self): self.assertRaises(TypeError, dhcp.DhcpBase, None) @@ -353,6 +369,15 @@ class TestDnsmasq(TestBase): self.safe.assert_called_once_with('/foo/opts', expected) + def test_output_opts_file_single_dhcp(self): + expected = 'tag:tag0,option:router,192.168.0.1' + with mock.patch.object(dhcp.Dnsmasq, 'get_conf_file_name') as conf_fn: + conf_fn.return_value = '/foo/opts' + dm = dhcp.Dnsmasq(self.conf, FakeDualNetworkSingleDHCP()) + dm._output_opts_file() + + self.safe.assert_called_once_with('/foo/opts', expected) + def test_reload_allocations(self): exp_host_name = '/dhcp/cccccccc-cccc-cccc-cccc-cccccccccccc/host' exp_host_data = """ -- 2.45.2