From 40fcbcb8852cad03eec345106772292fa08b25dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89douard=20Thuleau?= Date: Tue, 27 Nov 2012 17:15:34 +0100 Subject: [PATCH] Remove unused bridge interfaces If a network gateway of multi hosted network is unsed by any VM on the host, we tear down it. It is implemented only with the VLAN manager. Fixes LP bug #1084651 Change-Id: Ia7e399de5b8ddf6ec252d38dfbb8762c4f14e3a5 --- nova/db/api.py | 4 + nova/db/sqlalchemy/api.py | 5 ++ nova/network/linux_net.py | 117 ++++++++++++++++++++++++++- nova/network/manager.py | 29 ++++++- nova/tests/network/test_linux_net.py | 27 +++++++ nova/tests/test_db_api.py | 17 ++++ 6 files changed, 195 insertions(+), 4 deletions(-) diff --git a/nova/db/api.py b/nova/db/api.py index cfa6a6487711..67d8e7618c0b 100644 --- a/nova/db/api.py +++ b/nova/db/api.py @@ -809,6 +809,10 @@ def network_get_all_by_uuids(context, network_uuids, # pylint: disable=C0103 +def network_in_use_on_host(context, network_id, host=None): + """Indicates if a network is currently in use on host.""" + return IMPL.network_in_use_on_host(context, network_id, host) + def network_get_associated_fixed_ips(context, network_id, host=None): """Get all network's ips that have been associated.""" diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index 4155fdc3d97a..e1e2ede841e5 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -2236,6 +2236,11 @@ def network_get_associated_fixed_ips(context, network_id, host=None): return data +def network_in_use_on_host(context, network_id, host): + fixed_ips = network_get_associated_fixed_ips(context, network_id, host) + return len(fixed_ips) > 0 + + @require_admin_context def _network_get_query(context, session=None): return model_query(context, models.Network, session=session, diff --git a/nova/network/linux_net.py b/nova/network/linux_net.py index 7929c235cdd3..7059ecddb346 100644 --- a/nova/network/linux_net.py +++ b/nova/network/linux_net.py @@ -750,6 +750,18 @@ def _add_dnsmasq_accept_rules(dev): iptables_manager.apply() +def _remove_dnsmasq_accept_rules(dev): + """Remove DHCP and DNS traffic allowed through to dnsmasq.""" + table = iptables_manager.ipv4['filter'] + for port in [67, 53]: + for proto in ['udp', 'tcp']: + args = {'dev': dev, 'port': port, 'proto': proto} + table.remove_rule('INPUT', + '-i %(dev)s -p %(proto)s -m %(proto)s ' + '--dport %(port)s -j ACCEPT' % args) + iptables_manager.apply() + + def get_dhcp_opts(context, network_ref): """Get network's hosts config in dhcp-opts format.""" hosts = [] @@ -811,6 +823,7 @@ def kill_dhcp(dev): _execute('kill', '-9', pid, run_as_root=True) else: LOG.debug(_('Pid %d is stale, skip killing dnsmasq'), pid) + _remove_dnsmasq_accept_rules(dev) # NOTE(ja): Sending a HUP only reloads the hostfile, so any @@ -1138,7 +1151,21 @@ class LinuxBridgeInterfaceDriver(LinuxNetInterfaceDriver): iptables_manager.apply() return network['bridge'] - def unplug(self, network): + def unplug(self, network, gateway=True): + vlan = network.get('vlan') + if vlan is not None: + iface = 'vlan%s' % vlan + LinuxBridgeInterfaceDriver.remove_vlan_bridge(vlan, + network['bridge']) + else: + iface = CONF.flat_interface or network['bridge_interface'] + LinuxBridgeInterfaceDriver.remove_bridge(network['bridge'], + gateway) + + if CONF.share_dhcp_address: + remove_isolate_dhcp_address(iface, network['dhcp_server']) + + iptables_manager.apply() return self.get_dev(network) def get_dev(self, network): @@ -1154,7 +1181,13 @@ class LinuxBridgeInterfaceDriver(LinuxNetInterfaceDriver): return interface @classmethod - @lockutils.synchronized('ensure_vlan', 'nova-', external=True) + def remove_vlan_bridge(cls, vlan_num, bridge): + """Delete a bridge and vlan.""" + LinuxBridgeInterfaceDriver.remove_bridge(bridge) + LinuxBridgeInterfaceDriver.remove_vlan(vlan_num) + + @classmethod + @lockutils.synchronized('lock_vlan', 'nova-', external=True) def ensure_vlan(_self, vlan_num, bridge_interface, mac_address=None): """Create a vlan unless it already exists.""" interface = 'vlan%s' % vlan_num @@ -1179,7 +1212,24 @@ class LinuxBridgeInterfaceDriver(LinuxNetInterfaceDriver): return interface @classmethod - @lockutils.synchronized('ensure_bridge', 'nova-', external=True) + @lockutils.synchronized('lock_vlan', 'nova-', external=True) + def remove_vlan(cls, vlan_num): + """Delete a vlan""" + vlan_interface = 'vlan%s' % vlan_num + if not device_exists(vlan_interface): + return + else: + try: + utils.execute('ip', 'link', 'delete', vlan_interface, + run_as_root=True, check_exit_code=[0, 2, 254]) + except exception.ProcessExecutionError: + LOG.error(_("Failed unplugging VLAN interface '%s'"), + vlan_interface) + raise + LOG.debug(_("Unplugged VLAN interface '%s'"), vlan_interface) + + @classmethod + @lockutils.synchronized('lock_bridge', 'nova-', external=True) def ensure_bridge(_self, bridge, interface, net_attrs=None, gateway=True, filtering=True): """Create a bridge unless it already exists. @@ -1260,6 +1310,34 @@ class LinuxBridgeInterfaceDriver(LinuxNetInterfaceDriver): ipv4_filter.add_rule('FORWARD', '--out-interface %s -j DROP' % bridge) + @classmethod + @lockutils.synchronized('lock_bridge', 'nova-', external=True) + def remove_bridge(cls, bridge, gateway=True, filtering=True): + """Delete a bridge.""" + if not device_exists(bridge): + return + else: + if filtering: + ipv4_filter = iptables_manager.ipv4['filter'] + if gateway: + ipv4_filter.remove_rule('FORWARD', + '--in-interface %s -j ACCEPT' % bridge) + ipv4_filter.remove_rule('FORWARD', + '--out-interface %s -j ACCEPT' % bridge) + else: + ipv4_filter.remove_rule('FORWARD', + '--in-interface %s -j DROP' % bridge) + ipv4_filter.remove_rule('FORWARD', + '--out-interface %s -j DROP' % bridge) + try: + utils.execute('ip', 'link', 'delete', bridge, run_as_root=True, + check_exit_code=[0, 2, 254]) + except exception.ProcessExecutionError: + LOG.error(_("Failed unplugging bridge interface '%s'"), bridge) + raise + + LOG.debug(_("Unplugged bridge interface '%s'"), bridge) + @lockutils.synchronized('ebtables', 'nova-', external=True) def ensure_ebtables_rules(rules): @@ -1270,6 +1348,13 @@ def ensure_ebtables_rules(rules): _execute(*cmd, run_as_root=True) +@lockutils.synchronized('ebtables', 'nova-', external=True) +def remove_ebtables_rules(rules): + for rule in rules: + cmd = ['ebtables', '-D'] + rule.split() + _execute(*cmd, check_exit_code=False, run_as_root=True) + + def isolate_dhcp_address(interface, address): # block arp traffic to address accross the interface rules = [] @@ -1296,6 +1381,32 @@ def isolate_dhcp_address(interface, address): % (interface, address), top=True) +def remove_isolate_dhcp_address(interface, address): + # block arp traffic to address accross the interface + rules = [] + rules.append('INPUT -p ARP -i %s --arp-ip-dst %s -j DROP' + % (interface, address)) + rules.append('OUTPUT -p ARP -o %s --arp-ip-src %s -j DROP' + % (interface, address)) + remove_ebtables_rules(rules) + # NOTE(vish): the above is not possible with iptables/arptables + # block dhcp broadcast traffic across the interface + ipv4_filter = iptables_manager.ipv4['filter'] + ipv4_filter.remove_rule('FORWARD', + '-m physdev --physdev-in %s -d 255.255.255.255 ' + '-p udp --dport 67 -j DROP' % interface, top=True) + ipv4_filter.remove_rule('FORWARD', + '-m physdev --physdev-out %s -d 255.255.255.255 ' + '-p udp --dport 67 -j DROP' % interface, top=True) + # block ip traffic to address accross the interface + ipv4_filter.remove_rule('FORWARD', + '-m physdev --physdev-in %s -d %s -j DROP' + % (interface, address), top=True) + ipv4_filter.remove_rule('FORWARD', + '-m physdev --physdev-out %s -s %s -j DROP' + % (interface, address), top=True) + + # plugs interfaces using Open vSwitch class LinuxOVSInterfaceDriver(LinuxNetInterfaceDriver): diff --git a/nova/network/manager.py b/nova/network/manager.py index 6680634c9662..ace3d5765296 100644 --- a/nova/network/manager.py +++ b/nova/network/manager.py @@ -150,6 +150,11 @@ network_opts = [ cfg.BoolOpt('fake_call', default=False, help='If True, skip using the queue and make local calls'), + cfg.BoolOpt('teardown_unused_network_gateway', + default=False, + help='If True, unused gateway devices (VLAN and bridge) are ' + 'deleted in VLAN network mode with multi hosted ' + 'networks'), cfg.BoolOpt('force_dhcp_release', default=False, help='If True, send a dhcp release on instance termination'), @@ -1451,7 +1456,6 @@ class NetworkManager(manager.SchedulerDependentManager): if teardown: network = self._get_network_by_id(context, fixed_ip_ref['network_id']) - self._teardown_network_on_host(context, network) if CONF.force_dhcp_release: dev = self.driver.get_dev(network) @@ -1474,6 +1478,8 @@ class NetworkManager(manager.SchedulerDependentManager): # callback will get called by nova-dhcpbridge. self.driver.release_dhcp(dev, address, vif['address']) + self._teardown_network_on_host(context, network) + def lease_fixed_ip(self, context, address): """Called by dhcp-bridge when ip is leased.""" LOG.debug(_('Leased IP |%(address)s|'), locals(), context=context) @@ -2244,6 +2250,7 @@ class VlanManager(RPCAllocateFixedIP, FloatingIP, NetworkManager): return NetworkManager.create_networks( self, context, vpn=True, **kwargs) + @lockutils.synchronized('setup_network', 'nova-', external=True) def _setup_network_on_host(self, context, network): """Sets up network on this host.""" if not network['vpn_public_address']: @@ -2275,6 +2282,7 @@ class VlanManager(RPCAllocateFixedIP, FloatingIP, NetworkManager): self.db.network_update(context, network['id'], {'gateway_v6': gateway}) + @lockutils.synchronized('setup_network', 'nova-', external=True) def _teardown_network_on_host(self, context, network): if not CONF.fake_network: network['dhcp_server'] = self._get_dhcp_ip(context, network) @@ -2283,6 +2291,25 @@ class VlanManager(RPCAllocateFixedIP, FloatingIP, NetworkManager): elevated = context.elevated() self.driver.update_dhcp(elevated, dev, network) + # NOTE(ethuleau): For multi hosted networks, if the network is no + # more used on this host and if VPN forwarding rule aren't handed + # by the host, we delete the network gateway. + vpn_address = network['vpn_public_address'] + if (CONF.teardown_unused_network_gateway and + network['multi_host'] and vpn_address != CONF.vpn_ip and + not self.db.network_in_use_on_host(context, network['id'], + self.host)): + LOG.debug("Remove unused gateway %s", network['bridge']) + self.driver.kill_dhcp(dev) + self.l3driver.remove_gateway(network) + if not CONF.share_dhcp_address: + values = {'allocated': False, + 'host': None} + self.db.fixed_ip_update(context, network['dhcp_server'], + values) + else: + self.driver.update_dhcp(context, dev, network) + def _get_network_dict(self, network): """Returns the dict representing necessary and meta network fields""" diff --git a/nova/tests/network/test_linux_net.py b/nova/tests/network/test_linux_net.py index 55a9c7777442..68aaa6251f77 100644 --- a/nova/tests/network/test_linux_net.py +++ b/nova/tests/network/test_linux_net.py @@ -494,6 +494,33 @@ class LinuxNetworkTestCase(test.TestCase): for inp in expected_inputs: self.assertTrue(inp in inputs[0]) + executes = [] + inputs = [] + + @classmethod + def fake_remove(_self, bridge, gateway): + return + + self.stubs.Set(linux_net.LinuxBridgeInterfaceDriver, + 'remove_bridge', fake_remove) + + driver.unplug(network) + expected = [ + ('ebtables', '-D', 'INPUT', '-p', 'ARP', '-i', iface, + '--arp-ip-dst', dhcp, '-j', 'DROP'), + ('ebtables', '-D', 'OUTPUT', '-p', 'ARP', '-o', iface, + '--arp-ip-src', dhcp, '-j', 'DROP'), + ('iptables-save', '-c', '-t', 'filter'), + ('iptables-restore', '-c'), + ('iptables-save', '-c', '-t', 'nat'), + ('iptables-restore', '-c'), + ('ip6tables-save', '-c', '-t', 'filter'), + ('ip6tables-restore', '-c'), + ] + self.assertEqual(executes, expected) + for inp in expected_inputs: + self.assertFalse(inp in inputs[0]) + def _test_initialize_gateway(self, existing, expected, routes=''): self.flags(fake_network=False) executes = [] diff --git a/nova/tests/test_db_api.py b/nova/tests/test_db_api.py index f2124c0211fe..29bce8bf5cb4 100644 --- a/nova/tests/test_db_api.py +++ b/nova/tests/test_db_api.py @@ -582,6 +582,23 @@ class DbApiTestCase(test.TestCase): data = db.network_get_all_by_host(ctxt, 'foo') self.assertEqual(len(data), 3) + def test_network_in_use_on_host(self): + ctxt = context.get_admin_context() + + values = {'host': 'foo', 'hostname': 'myname'} + instance = db.instance_create(ctxt, values) + values = {'address': 'bar', 'instance_uuid': instance['uuid']} + vif = db.virtual_interface_create(ctxt, values) + values = {'address': 'baz', + 'network_id': 1, + 'allocated': True, + 'instance_uuid': instance['uuid'], + 'virtual_interface_id': vif['id']} + db.fixed_ip_create(ctxt, values) + + self.assertEqual(db.network_in_use_on_host(ctxt, 1, 'foo'), True) + self.assertEqual(db.network_in_use_on_host(ctxt, 1, 'bar'), False) + def _timeout_test(self, ctxt, timeout, multi_host): values = {'host': 'foo'} instance = db.instance_create(ctxt, values)