diff options
| author | Vishvananda Ishaya <vishvananda@gmail.com> | 2012-11-20 12:05:04 -0800 |
|---|---|---|
| committer | Vishvananda Ishaya <vishvananda@gmail.com> | 2012-11-26 15:17:16 -0800 |
| commit | 5b21ba723a4fe8076022dcafef0a55de3a99b35e (patch) | |
| tree | 9d7dfb7649987e0543220d821cc7d812a1a97224 | |
| parent | 1bd7b4248dd54c6486e8747074211566ba9c35c8 (diff) | |
Allow multi_host compute nodes to share dhcp ip
This adds a new flag: share_dhcp_address which if enabled in
multihost mode will allow all compute nodes to share an ip on
guest network. The code will isolate the address using iptables
and ebtables so it is only visible to the vms.
This patch has two benefits:
a) we don't have to use an ip address from every network for each
compute node. This is especially valuable in vlan mode where the
networks are generally small
b) we can improve security by blocking all access to the ip on the
guest network from outside the compute node. While we could do similar
blocking using a different ip for each node, it makes dhcp setup
much more complicated if a vm is migrated to another node.
Implements blueprint shared-dhcp-ip
Change-Id: Iaf84c0ad2848921122866956105eb44c074450dc
| -rw-r--r-- | etc/nova/rootwrap.d/network.filters | 5 | ||||
| -rw-r--r-- | nova/db/sqlalchemy/api.py | 11 | ||||
| -rw-r--r-- | nova/network/linux_net.py | 43 | ||||
| -rw-r--r-- | nova/network/manager.py | 6 | ||||
| -rw-r--r-- | nova/tests/network/test_linux_net.py | 64 | ||||
| -rw-r--r-- | nova/tests/test_db_api.py | 27 |
6 files changed, 152 insertions, 4 deletions
diff --git a/etc/nova/rootwrap.d/network.filters b/etc/nova/rootwrap.d/network.filters index c635f12e4..3a46080fa 100644 --- a/etc/nova/rootwrap.d/network.filters +++ b/etc/nova/rootwrap.d/network.filters @@ -34,6 +34,11 @@ ovs-vsctl: CommandFilter, /usr/bin/ovs-vsctl, root # nova/network/linux_net.py: 'ovs-ofctl', .... ovs-ofctl: CommandFilter, /usr/bin/ovs-ofctl, root +# nova/network/linux_net.py: 'ebtables', '-D' ... +# nova/network/linux_net.py: 'ebtables', '-I' ... +ebtables: CommandFilter, /sbin/ebtables, root +ebtables_usr: CommandFilter, /usr/sbin/ebtables, root + # nova/network/linux_net.py: 'ip[6]tables-save' % (cmd, '-t', ... iptables-save: CommandFilter, /sbin/iptables-save, root iptables-save_usr: CommandFilter, /usr/sbin/iptables-save, root diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index 34fbec6d3..d3c38e79b 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -2291,11 +2291,20 @@ def network_get_all_by_instance(context, instance_id): @require_admin_context def network_get_all_by_host(context, host): session = get_session() + fixed_host_filter = or_(models.FixedIp.host == host, + models.Instance.host == host) fixed_ip_query = model_query(context, models.FixedIp.network_id, session=session).\ - filter(models.FixedIp.host == host) + outerjoin((models.VirtualInterface, + models.VirtualInterface.id == + models.FixedIp.virtual_interface_id)).\ + outerjoin((models.Instance, + models.Instance.uuid == + models.VirtualInterface.instance_uuid)).\ + filter(fixed_host_filter) # NOTE(vish): return networks that have host set # or that have a fixed ip with host set + # or that have an instance with host set host_filter = or_(models.Network.host == host, models.Network.id.in_(fixed_ip_query.subquery())) return _network_get_query(context, session=session).\ diff --git a/nova/network/linux_net.py b/nova/network/linux_net.py index c57e9a730..c64864f09 100644 --- a/nova/network/linux_net.py +++ b/nova/network/linux_net.py @@ -1085,14 +1085,16 @@ class LinuxNetInterfaceDriver(object): class LinuxBridgeInterfaceDriver(LinuxNetInterfaceDriver): def plug(self, network, mac_address, gateway=True): - if network.get('vlan', None) is not None: + vlan = network.get('vlan') + if vlan is not None: iface = CONF.vlan_interface or network['bridge_interface'] LinuxBridgeInterfaceDriver.ensure_vlan_bridge( - network['vlan'], + vlan, network['bridge'], iface, network, mac_address) + iface = 'vlan%s' % vlan else: iface = CONF.flat_interface or network['bridge_interface'] LinuxBridgeInterfaceDriver.ensure_bridge( @@ -1100,6 +1102,8 @@ class LinuxBridgeInterfaceDriver(LinuxNetInterfaceDriver): iface, network, gateway) + if CONF.share_dhcp_address: + isolate_dhcp_address(iface, network['dhcp_server']) # NOTE(vish): applying here so we don't get a lock conflict iptables_manager.apply() return network['bridge'] @@ -1227,6 +1231,41 @@ class LinuxBridgeInterfaceDriver(LinuxNetInterfaceDriver): '--out-interface %s -j DROP' % bridge) +@lockutils.synchronized('ebtables', 'nova-', external=True) +def ensure_ebtables_rules(rules): + for rule in rules: + cmd = ['ebtables', '-D'] + rule.split() + _execute(*cmd, check_exit_code=False, run_as_root=True) + cmd[1] = '-I' + _execute(*cmd, run_as_root=True) + + +def 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)) + # NOTE(vish): the above is not possible with iptables/arptables + ensure_ebtables_rules(rules) + # block dhcp broadcast traffic across the interface + ipv4_filter = iptables_manager.ipv4['filter'] + ipv4_filter.add_rule('FORWARD', + '-m physdev --physdev-in %s -d 255.255.255.255 ' + '-p udp --dport 67 -j DROP' % interface, top=True) + ipv4_filter.add_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.add_rule('FORWARD', + '-m physdev --physdev-in %s -d %s -j DROP' + % (interface, address), top=True) + ipv4_filter.add_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 f54764d8f..d380548c6 100644 --- a/nova/network/manager.py +++ b/nova/network/manager.py @@ -153,6 +153,10 @@ network_opts = [ cfg.BoolOpt('force_dhcp_release', default=False, help='If True, send a dhcp release on instance termination'), + cfg.BoolOpt('share_dhcp_address', + default=False, + help='If True in multi_host mode, all compute hosts share ' + 'the same dhcp address.'), cfg.StrOpt('dhcp_domain', default='novalocal', help='domain to use for building the hostnames'), @@ -894,7 +898,7 @@ class NetworkManager(manager.SchedulerDependentManager): def _get_dhcp_ip(self, context, network_ref, host=None): """Get the proper dhcp address to listen on.""" # NOTE(vish): this is for compatibility - if not network_ref.get('multi_host'): + if not network_ref.get('multi_host') or CONF.share_dhcp_address: return network_ref['gateway'] if not host: diff --git a/nova/tests/network/test_linux_net.py b/nova/tests/network/test_linux_net.py index 666ce6dab..757b73a94 100644 --- a/nova/tests/network/test_linux_net.py +++ b/nova/tests/network/test_linux_net.py @@ -405,6 +405,70 @@ class LinuxNetworkTestCase(test.TestCase): driver.plug(network, "fakemac") self.assertEqual(info['passed_interface'], "override_interface") + def test_isolated_host(self): + self.flags(fake_network=False, + share_dhcp_address=True) + # NOTE(vish): use a fresh copy of the manager for each test + self.stubs.Set(linux_net, 'iptables_manager', + linux_net.IptablesManager()) + self.stubs.Set(linux_net, 'binary_name', 'test') + executes = [] + inputs = [] + + def fake_execute(*args, **kwargs): + executes.append(args) + process_input = kwargs.get('process_input') + if process_input: + inputs.append(process_input) + return "", "" + + self.stubs.Set(utils, 'execute', fake_execute) + + driver = linux_net.LinuxBridgeInterfaceDriver() + + @classmethod + def fake_ensure(_self, bridge, interface, network, gateway): + return bridge + + self.stubs.Set(linux_net.LinuxBridgeInterfaceDriver, + 'ensure_bridge', fake_ensure) + + iface = 'eth0' + dhcp = '192.168.1.1' + network = {'dhcp_server': dhcp, + 'bridge': 'br100', + 'bridge_interface': iface} + driver.plug(network, 'fakemac') + expected = [ + ('ebtables', '-D', 'INPUT', '-p', 'ARP', '-i', iface, + '--arp-ip-dst', dhcp, '-j', 'DROP'), + ('ebtables', '-I', 'INPUT', '-p', 'ARP', '-i', iface, + '--arp-ip-dst', dhcp, '-j', 'DROP'), + ('ebtables', '-D', 'OUTPUT', '-p', 'ARP', '-o', iface, + '--arp-ip-src', dhcp, '-j', 'DROP'), + ('ebtables', '-I', '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) + expected_inputs = [ + '-A test-FORWARD -m physdev --physdev-in %s ' + '-d 255.255.255.255 -p udp --dport 67 -j DROP' % iface, + '-A test-FORWARD -m physdev --physdev-out %s ' + '-d 255.255.255.255 -p udp --dport 67 -j DROP' % iface, + '-A test-FORWARD -m physdev --physdev-in %s ' + '-d 192.168.1.1 -j DROP' % iface, + '-A test-FORWARD -m physdev --physdev-out %s ' + '-s 192.168.1.1 -j DROP' % iface, + ] + for inp in expected_inputs: + self.assertTrue(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 193378557..f54af15dd 100644 --- a/nova/tests/test_db_api.py +++ b/nova/tests/test_db_api.py @@ -469,6 +469,33 @@ class DbApiTestCase(test.TestCase): data = db.network_get_associated_fixed_ips(ctxt, 1, 'nothing') self.assertEqual(len(data), 0) + def test_network_get_all_by_host(self): + ctxt = context.get_admin_context() + data = db.network_get_all_by_host(ctxt, 'foo') + self.assertEqual(len(data), 0) + # dummy network + net = db.network_create_safe(ctxt, {}) + # network with host set + net = db.network_create_safe(ctxt, {'host': 'foo'}) + data = db.network_get_all_by_host(ctxt, 'foo') + self.assertEqual(len(data), 1) + # network with fixed ip with host set + net = db.network_create_safe(ctxt, {}) + values = {'host': 'foo', 'network_id': net['id']} + fixed_address = db.fixed_ip_create(ctxt, values) + data = db.network_get_all_by_host(ctxt, 'foo') + self.assertEqual(len(data), 2) + # network with instance with host set + net = db.network_create_safe(ctxt, {}) + instance = db.instance_create(ctxt, {'host': 'foo'}) + values = {'instance_uuid': instance['uuid']} + vif = db.virtual_interface_create(ctxt, values) + values = {'network_id': net['id'], + 'virtual_interface_id': vif['id']} + fixed_address = db.fixed_ip_create(ctxt, values) + data = db.network_get_all_by_host(ctxt, 'foo') + self.assertEqual(len(data), 3) + def _timeout_test(self, ctxt, timeout, multi_host): values = {'host': 'foo'} instance = db.instance_create(ctxt, values) |
