From 5a470f89b6a508d578b89a1687d327efbc834346 Mon Sep 17 00:00:00 2001 From: Dan Wendlandt Date: Thu, 30 Aug 2012 22:21:51 -0700 Subject: fix issues with Nova security groups and Quantum bug #1039400 - make quantumv2/api.py fetch actual DHCP server address, which is needed by firewall layer (otherwise, the gateway IP is incorrectly used and all DHCP traffic is dropped). - add missing call from quantumv2/api.py to the security groups API when a VM is allocated/deallocated. - Add a vif-driver that is a hybrid of the existing Open vswitch + linux bridge drivers, which allows OVS quantum plugins to be compatible with iptables based filtering, in particular, nova security groups. - Also clean-up some docstrings in virt/libvirt/vif.py Change-Id: I7cf5cf09583202a12785b616d18db3ee4bbffee0 --- nova/network/linux_net.py | 20 ++++++ nova/network/quantumv2/api.py | 34 +++++++++- nova/tests/network/test_quantumv2.py | 21 +++++- nova/tests/test_libvirt_vif.py | 16 +++++ nova/virt/libvirt/vif.py | 120 ++++++++++++++++++++++++++++------- 5 files changed, 183 insertions(+), 28 deletions(-) diff --git a/nova/network/linux_net.py b/nova/network/linux_net.py index 40f75f2ff..b9988588c 100644 --- a/nova/network/linux_net.py +++ b/nova/network/linux_net.py @@ -997,6 +997,26 @@ def _ip_bridge_cmd(action, params, device): return cmd +def _create_veth_pair(dev1_name, dev2_name): + """Create a pair of veth devices with the specified names, + deleting any previous devices with those names. + """ + for dev in [dev1_name, dev2_name]: + if _device_exists(dev): + try: + utils.execute('ip', 'link', 'delete', dev1_name, + run_as_root=True, check_exit_code=[0, 2, 254]) + except exception.ProcessExecutionError: + LOG.exception("Error clearing stale veth %s" % dev) + + utils.execute('ip', 'link', 'add', dev1_name, 'type', 'veth', 'peer', + 'name', dev2_name, run_as_root=True) + for dev in [dev1_name, dev2_name]: + utils.execute('ip', 'link', 'set', dev, 'up', run_as_root=True) + utils.execute('ip', 'link', 'set', dev, 'promisc', 'on', + run_as_root=True) + + # Similar to compute virt layers, the Linux network node # code uses a flexible driver model to support different ways # of creating ethernet interfaces and attaching them to the network. diff --git a/nova/network/quantumv2/api.py b/nova/network/quantumv2/api.py index 990af4c37..8689e8188 100644 --- a/nova/network/quantumv2/api.py +++ b/nova/network/quantumv2/api.py @@ -56,6 +56,8 @@ LOG = logging.getLogger(__name__) class API(base.Base): """API for interacting with the quantum 2.x API.""" + security_group_api = None + def setup_networks_on_host(self, context, instance, host=None, teardown=False): """Setup or teardown the network structures.""" @@ -131,6 +133,9 @@ class API(base.Base): " failure: %(exception)s") LOG.debug(msg, {'portid': port_id, 'exception': ex}) + + self.trigger_security_group_members_refresh(context, instance) + return self.get_instance_nw_info(context, instance, networks=nets) def deallocate_for_instance(self, context, instance, **kwargs): @@ -146,6 +151,7 @@ class API(base.Base): except Exception as ex: LOG.exception(_("Failed to delete quantum port %(portid)s ") % {'portid': port['id']}) + self.trigger_security_group_members_refresh(context, instance) @refresh_cache def get_instance_nw_info(self, context, instance, networks=None): @@ -219,6 +225,21 @@ class API(base.Base): return [{'instance_uuid': port['device_id']} for port in ports if port['device_id']] + def trigger_security_group_members_refresh(self, context, instance_ref): + + # used to avoid circular import + if not self.security_group_api: + from nova.compute import api as compute_api + self.security_group_api = compute_api.SecurityGroupAPI() + + admin_context = context.elevated() + group_ids = [group['id'] for group in instance_ref['security_groups']] + + self.security_group_api.trigger_members_refresh(admin_context, + group_ids) + self.security_group_api.trigger_handler('security_group_members', + admin_context, group_ids) + @refresh_cache def associate_floating_ip(self, context, instance, floating_address, fixed_address, @@ -339,13 +360,24 @@ class API(base.Base): data = quantumv2.get_client(context).list_subnets(**search_opts) ipam_subnets = data.get('subnets', []) subnets = [] + for subnet in ipam_subnets: subnet_dict = {'cidr': subnet['cidr'], 'gateway': network_model.IP( address=subnet['gateway_ip'], type='gateway'), } - # TODO(gongysh) deal with dhcp + + # attempt to populate DHCP server field + search_opts = {'network_id': subnet['network_id'], + 'device_owner': 'network:dhcp'} + data = quantumv2.get_client(context).list_ports(**search_opts) + dhcp_ports = data.get('ports', []) + for p in dhcp_ports: + for ip_pair in p['fixed_ips']: + if ip_pair['subnet_id'] == subnet['id']: + subnet_dict['dhcp_server'] = ip_pair['ip_address'] + break subnet_object = network_model.Subnet(**subnet_dict) for dns in subnet.get('dns_nameservers', []): diff --git a/nova/tests/network/test_quantumv2.py b/nova/tests/network/test_quantumv2.py index 7ee814b6b..6c72b9c6a 100644 --- a/nova/tests/network/test_quantumv2.py +++ b/nova/tests/network/test_quantumv2.py @@ -143,7 +143,8 @@ class TestQuantumv2(test.TestCase): 'bff4a5a6b9eb4ea2a6efec6eefb77936') self.instance = {'project_id': '9d049e4b60b64716978ab415e6fbd5c0', 'uuid': str(utils.gen_uuid()), - 'display_name': 'test_instance'} + 'display_name': 'test_instance', + 'security_groups': []} self.nets1 = [{'id': 'my_netid1', 'name': 'my_netname1', 'tenant_id': 'my_tenantid'}] @@ -164,6 +165,8 @@ class TestQuantumv2(test.TestCase): 'fixed_ips': [{'ip_address': '10.0.1.2', 'subnet_id': 'my_subid1'}], 'mac_address': 'my_mac1', }] + self.dhcp_port_data1 = [{'fixed_ips': [{'ip_address': '10.0.1.9', + 'subnet_id': 'my_subid1'}]}] self.port_data2 = [] self.port_data2.append(self.port_data1[0]) self.port_data2.append({'network_id': 'my_netid2', @@ -173,11 +176,15 @@ class TestQuantumv2(test.TestCase): 'fixed_ips': [{'ip_address': '10.0.2.2', 'subnet_id': 'my_subid2'}], 'mac_address': 'my_mac2', }) - self.subnet_data1 = [{'cidr': '10.0.1.0/24', + self.subnet_data1 = [{'id': 'my_subid1', + 'cidr': '10.0.1.0/24', + 'network_id': 'my_netid1', 'gateway_ip': '10.0.1.1', 'dns_nameservers': ['8.8.1.1', '8.8.1.2']}] self.subnet_data2 = [] - self.subnet_data2.append({'cidr': '10.0.2.0/24', + self.subnet_data2.append({'id': 'my_subid2', + 'cidr': '10.0.2.0/24', + 'network_id': 'my_netid2', 'gateway_ip': '10.0.2.1', 'dns_nameservers': ['8.8.2.1', '8.8.2.2']}) @@ -221,6 +228,10 @@ class TestQuantumv2(test.TestCase): self.moxed_client.list_subnets( id=mox.SameElementsAs(['my_subid%s' % i])).AndReturn( {'subnets': subnet_data}) + self.moxed_client.list_ports( + network_id=subnet_data[0]['network_id'], + device_owner='network:dhcp').AndReturn( + {'ports': []}) self.mox.ReplayAll() nw_inf = api.get_instance_nw_info(self.context, self.instance) for i in xrange(0, number): @@ -248,6 +259,10 @@ class TestQuantumv2(test.TestCase): self.moxed_client.list_subnets( id=mox.SameElementsAs(['my_subid1'])).AndReturn( {'subnets': self.subnet_data1}) + self.moxed_client.list_ports( + network_id='my_netid1', + device_owner='network:dhcp').AndReturn( + {'ports': self.dhcp_port_data1}) self.mox.ReplayAll() nw_inf = api.get_instance_nw_info(self.context, self.instance, diff --git a/nova/tests/test_libvirt_vif.py b/nova/tests/test_libvirt_vif.py index 9e5ce24bd..ca52f14ed 100644 --- a/nova/tests/test_libvirt_vif.py +++ b/nova/tests/test_libvirt_vif.py @@ -154,3 +154,19 @@ class LibvirtVifTestCase(test.TestCase): self.assertEquals(script, "") d.unplug(None, (self.net, self.mapping)) + + def test_quantum_hybrid_driver(self): + d = vif.LibvirtHybridOVSBridgeDriver() + xml = self._get_instance_xml(d) + + doc = etree.fromstring(xml) + ret = doc.findall('./devices/interface') + self.assertEqual(len(ret), 1) + node = ret[0] + self.assertEqual(node.get("type"), "bridge") + br_name = node.find("source").get("bridge") + self.assertEqual(br_name, self.net['bridge']) + mac = node.find("mac").get("address") + self.assertEqual(mac, self.mapping['mac']) + + d.unplug(None, (self.net, self.mapping)) diff --git a/nova/virt/libvirt/vif.py b/nova/virt/libvirt/vif.py index 0337ddc89..1a64765b1 100644 --- a/nova/virt/libvirt/vif.py +++ b/nova/virt/libvirt/vif.py @@ -45,6 +45,8 @@ FLAGS = flags.FLAGS FLAGS.register_opts(libvirt_vif_opts) flags.DECLARE('libvirt_type', 'nova.virt.libvirt.driver') +LINUX_DEV_LEN = 14 + class LibvirtBridgeDriver(vif.VIFDriver): """VIF driver for Linux bridge.""" @@ -114,12 +116,29 @@ class LibvirtBridgeDriver(vif.VIFDriver): class LibvirtOpenVswitchDriver(vif.VIFDriver): - """VIF driver for Open vSwitch that uses type='ethernet' - libvirt XML. Used for libvirt versions that do not support - OVS virtual port XML (0.9.10 or earlier).""" + """VIF driver for Open vSwitch that uses libivrt type='ethernet' + + Used for libvirt versions that do not support + OVS virtual port XML (0.9.10 or earlier). + """ def get_dev_name(self, iface_id): - return "tap" + iface_id[0:11] + return ("tap" + iface_id)[:LINUX_DEV_LEN] + + def create_ovs_vif_port(self, dev, iface_id, mac, instance_id): + utils.execute('ovs-vsctl', '--', '--may-exist', 'add-port', + FLAGS.libvirt_ovs_bridge, dev, + '--', 'set', 'Interface', dev, + 'external-ids:iface-id=%s' % iface_id, + 'external-ids:iface-status=active', + 'external-ids:attached-mac=%s' % mac, + 'external-ids:vm-uuid=%s' % instance_id, + run_as_root=True) + + def delete_ovs_vif_port(self, dev): + utils.execute('ovs-vsctl', 'del-port', FLAGS.libvirt_ovs_bridge, + dev, run_as_root=True) + utils.execute('ip', 'link', 'delete', dev, run_as_root=True) def plug(self, instance, vif): network, mapping = vif @@ -138,17 +157,9 @@ class LibvirtOpenVswitchDriver(vif.VIFDriver): # Second option: tunctl utils.execute('tunctl', '-b', '-t', dev, run_as_root=True) utils.execute('ip', 'link', 'set', dev, 'up', run_as_root=True) - utils.execute('ovs-vsctl', '--', '--may-exist', 'add-port', - FLAGS.libvirt_ovs_bridge, dev, - '--', 'set', 'Interface', dev, - "external-ids:iface-id=%s" % iface_id, - '--', 'set', 'Interface', dev, - "external-ids:iface-status=active", - '--', 'set', 'Interface', dev, - "external-ids:attached-mac=%s" % mapping['mac'], - '--', 'set', 'Interface', dev, - "external-ids:vm-uuid=%s" % instance['uuid'], - run_as_root=True) + + self.create_ovs_vif_port(dev, iface_id, mapping['mac'], + instance['uuid']) conf = config.LibvirtConfigGuestInterface() @@ -162,14 +173,76 @@ class LibvirtOpenVswitchDriver(vif.VIFDriver): return conf def unplug(self, instance, vif): - """Unplug the VIF from the network by deleting the port from - the bridge.""" + """Unplug the VIF by deleting the port from the bridge.""" + try: + network, mapping = vif + self.delete_ovs_vif_port(self.get_dev_name(mapping['vif_uuid'])) + except exception.ProcessExecutionError: + LOG.exception(_("Failed while unplugging vif"), instance=instance) + + +class LibvirtHybridOVSBridgeDriver(LibvirtBridgeDriver, + LibvirtOpenVswitchDriver): + """VIF driver that uses OVS + Linux Bridge for iptables compatibility. + + Enables the use of OVS-based Quantum plugins while at the same + time using iptables-based filtering, which requires that vifs be + plugged into a linux bridge, not OVS. IPtables filtering is useful for + in particular for Nova security groups. + """ + + def get_br_name(self, iface_id): + return ("qbr" + iface_id)[:LINUX_DEV_LEN] + + def get_veth_pair_names(self, iface_id): + return (("qvb%s" % iface_id)[:LINUX_DEV_LEN], + ("qvo%s" % iface_id)[:LINUX_DEV_LEN]) + + def plug(self, instance, vif): + """Plug using hybrid strategy + + Create a per-VIF linux bridge, then link that bridge to the OVS + integration bridge via a veth device, setting up the other end + of the veth device just like a normal OVS port. Then boot the + VIF on the linux bridge using standard libvirt mechanisms + """ + network, mapping = vif - dev = self.get_dev_name(mapping['vif_uuid']) + iface_id = mapping['vif_uuid'] + br_name = self.get_br_name(iface_id) + v1_name, v2_name = self.get_veth_pair_names(iface_id) + + linux_net._create_veth_pair(v1_name, v2_name) + + if not linux_net._device_exists(br_name): + utils.execute('brctl', 'addbr', br_name, run_as_root=True) + + utils.execute('ip', 'link', 'set', br_name, 'up', run_as_root=True) + utils.execute('brctl', 'addif', br_name, v1_name, run_as_root=True) + self.create_ovs_vif_port(v2_name, iface_id, mapping['mac'], + instance['uuid']) + + network['bridge'] = br_name + return self._get_configurations(instance, network, mapping) + + def unplug(self, instance, vif): + """UnPlug using hybrid strategy + + Unhook port from OVS, unhook port from bridge, delete + bridge, and delete both veth devices. + """ try: - utils.execute('ovs-vsctl', 'del-port', - FLAGS.libvirt_ovs_bridge, dev, run_as_root=True) - utils.execute('ip', 'link', 'delete', dev, run_as_root=True) + network, mapping = vif + iface_id = mapping['vif_uuid'] + br_name = self.get_br_name(iface_id) + v1_name, v2_name = self.get_veth_pair_names(iface_id) + + utils.execute('brctl', 'delif', br_name, v1_name, run_as_root=True) + utils.execute('ip', 'link', 'set', br_name, 'down', + run_as_root=True) + utils.execute('brctl', 'delbr', br_name, run_as_root=True) + + self.delete_ovs_vif_port(v2_name) except exception.ProcessExecutionError: LOG.exception(_("Failed while unplugging vif"), instance=instance) @@ -203,7 +276,7 @@ class QuantumLinuxBridgeVIFDriver(vif.VIFDriver): """VIF driver for Linux Bridge when running Quantum.""" def get_dev_name(self, iface_id): - return "tap" + iface_id[0:11] + return ("tap" + iface_id)[:LINUX_DEV_LEN] def plug(self, instance, vif): network, mapping = vif @@ -225,8 +298,7 @@ class QuantumLinuxBridgeVIFDriver(vif.VIFDriver): return conf def unplug(self, instance, vif): - """Unplug the VIF from the network by deleting the port from - the bridge.""" + """Unplug the VIF by deleting the port from the bridge.""" network, mapping = vif dev = self.get_dev_name(mapping['vif_uuid']) try: -- cgit